diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs index 7c800347..6266db5c 100644 --- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs +++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs @@ -469,14 +469,14 @@ public static class DeviceManager CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine); foreach (var routingInputPort in inputPorts) { - CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, CrestronEnvironment.NewLine); + CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingInputPort.Key, routingInputPort.Type, CrestronEnvironment.NewLine); } } if (outputPorts == null) return; CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine); foreach (var routingOutputPort in outputPorts) { - CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine); + CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingOutputPort.Key, routingOutputPort.Type, CrestronEnvironment.NewLine); } } diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index b4335074..e2b30eee 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -17,17 +17,129 @@ namespace PepperDash.Essentials.Core; /// public static class Extensions { + /// /// Stores pending route requests, keyed by the destination device key. /// Used primarily to handle routing requests while a device is cooling down. /// private static readonly Dictionary RouteRequests = new Dictionary(); + + /// + /// A collection of RouteDescriptors for each signal type. + /// + public static readonly Dictionary RouteDescriptors = new Dictionary() + { + { eRoutingSignalType.Audio, new RouteDescriptorCollection() }, + { eRoutingSignalType.Video, new RouteDescriptorCollection() }, + { eRoutingSignalType.AudioVideo, new RouteDescriptorCollection() } + }; + /// /// A queue to process route requests and releases sequentially. /// private static readonly GenericQueue routeRequestQueue = new GenericQueue("routingQueue"); + /// + /// Indexed lookup of TieLines by destination device key for faster queries. + /// + private static Dictionary> _tieLinesByDestination; + + /// + /// Indexed lookup of TieLines by source device key for faster queries. + /// + private static Dictionary> _tieLinesBySource; + + /// + /// Cache of failed route attempts to avoid re-checking impossible paths. + /// Format: "sourceKey|destKey|signalType" + /// + private static readonly HashSet _impossibleRoutes = new HashSet(); + + /// + /// Indexes all TieLines by source and destination device keys for faster lookups. + /// Should be called once at system startup after all TieLines are created. + /// + public static void IndexTieLines() + { + try + { + Debug.LogMessage(LogEventLevel.Information, "Indexing TieLines for faster route discovery"); + + _tieLinesByDestination = TieLineCollection.Default + .GroupBy(t => t.DestinationPort.ParentDevice.Key) + .ToDictionary(g => g.Key, g => g.ToList()); + + _tieLinesBySource = TieLineCollection.Default + .GroupBy(t => t.SourcePort.ParentDevice.Key) + .ToDictionary(g => g.Key, g => g.ToList()); + + Debug.LogMessage(LogEventLevel.Information, "TieLine indexing complete. {0} destination keys, {1} source keys", + null, _tieLinesByDestination.Count, _tieLinesBySource.Count); + } + catch (Exception ex) + { + Debug.LogError("Exception indexing TieLines: {exception}", ex.Message); + Debug.LogDebug(ex, "Stack Trace: "); + } + } + + /// + /// Gets TieLines connected to a destination device. + /// Uses indexed lookup if available, otherwise falls back to LINQ query. + /// + /// The destination device key + /// List of TieLines connected to the destination + private static IEnumerable GetTieLinesForDestination(string destinationKey) + { + if (_tieLinesByDestination != null && _tieLinesByDestination.TryGetValue(destinationKey, out List tieLines)) + { + return tieLines; + } + + // Fallback to LINQ if index not available + return TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destinationKey); + } + + /// + /// Gets TieLines connected to a source device. + /// Uses indexed lookup if available, otherwise falls back to LINQ query. + /// + /// The source device key + /// List of TieLines connected to the source + private static IEnumerable GetTieLinesForSource(string sourceKey) + { + if (_tieLinesBySource != null && _tieLinesBySource.TryGetValue(sourceKey, out List tieLines)) + { + return tieLines; + } + + // Fallback to LINQ if index not available + return TieLineCollection.Default.Where(t => t.SourcePort.ParentDevice.Key == sourceKey); + } + + /// + /// Creates a cache key for route impossibility tracking. + /// + /// Source device key + /// Destination device key + /// Signal type + /// Cache key string + private static string GetRouteKey(string sourceKey, string destKey, eRoutingSignalType type) + { + return string.Format("{0}|{1}|{2}", sourceKey, destKey, type); + } + + /// + /// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime. + /// + public static void ClearImpossibleRoutesCache() + { + _impossibleRoutes.Clear(); + Debug.LogMessage(LogEventLevel.Information, "Impossible routes cache cleared"); + } + + /// /// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute /// and then attempts a new Route and if sucessful, stores that RouteDescriptor @@ -108,8 +220,7 @@ public static class Extensions public static (RouteDescriptor, RouteDescriptor) GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort, RoutingOutputPort sourcePort) { // if it's a single signal type, find the route - if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) && - !(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio))) + if (!signalType.HasFlag(eRoutingSignalType.AudioVideo)) { var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType); Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key, signalType); @@ -129,17 +240,9 @@ public static class Extensions Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {sourceKey} of type {type}", destination, source.Key); - RouteDescriptor audioRouteDescriptor; + var audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio); - if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio)) - { - audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio); - } else - { - audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio); - } - - var audioSuccess = destination.GetRouteToSource(source, null, null, signalType.HasFlag(eRoutingSignalType.SecondaryAudio) ? eRoutingSignalType.SecondaryAudio : eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort); + var audioSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort); if (!audioSuccess) Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key); @@ -165,10 +268,12 @@ public static class Extensions if (!audioSuccess && !videoSuccess) return (null, null); - - return (audioRouteDescriptor, videoRouteDescriptor); + // Return null for descriptors that have no routes + return (audioSuccess && audioRouteDescriptor.Routes.Count > 0 ? audioRouteDescriptor : null, + videoSuccess && videoRouteDescriptor.Routes.Count > 0 ? videoRouteDescriptor : null); } + /// /// Internal method to handle the logic for releasing an existing route and making a new one. /// Handles devices with cooling states by queueing the request. @@ -192,13 +297,13 @@ public static class Extensions Source = source, SourcePort = sourcePort, SignalType = signalType - }; + }; var coolingDevice = destination as IWarmingCooling; //We already have a route request for this device, and it's a cooling device and is cooling if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRouteRequest) && coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) - { + { coolingDevice.IsCoolingDownFeedback.OutputChange -= existingRouteRequest.HandleCooldown; coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; @@ -212,7 +317,7 @@ public static class Extensions //New Request if (coolingDevice != null && coolingDevice.IsCoolingDownFeedback.BoolValue == true) - { + { coolingDevice.IsCoolingDownFeedback.OutputChange += routeRequest.HandleCooldown; RouteRequests.Add(destination.Key, routeRequest); @@ -232,9 +337,9 @@ public static class Extensions Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key); } - routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination,destinationPort?.Key ?? string.Empty, false)); + routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false)); - routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest)); + routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest)); } /// @@ -249,7 +354,48 @@ public static class Extensions if (request.Source == null) return; - var (audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort); + RouteDescriptor audioOrSingleRoute = null; + RouteDescriptor videoRoute = null; + + // Try to use pre-loaded route descriptors first + if (request.SignalType.HasFlag(eRoutingSignalType.AudioVideo)) + { + // For AudioVideo routes, check both Audio and Video collections + if (RouteDescriptors.TryGetValue(eRoutingSignalType.Audio, out RouteDescriptorCollection audioCollection)) + { + audioOrSingleRoute = audioCollection.Descriptors.FirstOrDefault(d => + d.Source.Key == request.Source.Key && + d.Destination.Key == request.Destination.Key && + (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key)); + } + + if (RouteDescriptors.TryGetValue(eRoutingSignalType.Video, out RouteDescriptorCollection videoCollection)) + { + videoRoute = videoCollection.Descriptors.FirstOrDefault(d => + d.Source.Key == request.Source.Key && + d.Destination.Key == request.Destination.Key && + (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key)); + } + } + else + { + // For single signal type routes + if (RouteDescriptors.TryGetValue(request.SignalType, out RouteDescriptorCollection collection)) + { + audioOrSingleRoute = collection.Descriptors.FirstOrDefault(d => + d.Source.Key == request.Source.Key && + d.Destination.Key == request.Destination.Key && + (request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key)); + } + } + + // If no pre-loaded route found, build it dynamically + if (audioOrSingleRoute == null && videoRoute == null) + { + Debug.LogMessage(LogEventLevel.Debug, "No pre-loaded route found, building dynamically", request.Destination); + (audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort); + } + if (audioOrSingleRoute == null && videoRoute == null) return; @@ -265,7 +411,8 @@ public static class Extensions audioOrSingleRoute.ExecuteRoutes(); videoRoute?.ExecuteRoutes(); - } catch(Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request); } @@ -298,9 +445,86 @@ public static class Extensions Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key); current.ReleaseRoutes(clearRoute); } - } catch (Exception ex) + } + catch (Exception ex) { - Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'",null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); + Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'", null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); + } + } + + /// + /// Maps destination input ports to source output ports for all routing devices. + /// + public static void MapDestinationsToSources() + { + try + { + // Index TieLines before mapping if not already done + if (_tieLinesByDestination == null || _tieLinesBySource == null) + { + IndexTieLines(); + } + + var sinks = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); + var sources = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); + + foreach (var sink in sinks) + { + foreach (var source in sources) + { + foreach (var inputPort in sink.InputPorts) + { + foreach (var outputPort in source.OutputPorts) + { + var (audioOrSingleRoute, videoRoute) = sink.GetRouteToSource(source, inputPort.Type, inputPort, outputPort); + + if (audioOrSingleRoute == null && videoRoute == null) + { + continue; + } + + if (audioOrSingleRoute != null) + { + // Only add routes that have actual switching steps + if (audioOrSingleRoute.Routes == null || audioOrSingleRoute.Routes.Count == 0) + { + continue; + } + + // Add to the appropriate collection(s) based on signal type + // Note: A single route descriptor with combined flags (e.g., AudioVideo) will be added once per matching signal type + if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Audio)) + { + RouteDescriptors[eRoutingSignalType.Audio].AddRouteDescriptor(audioOrSingleRoute); + } + if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Video)) + { + RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(audioOrSingleRoute); + } + if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Usb)) + { + RouteDescriptors[eRoutingSignalType.Usb].AddRouteDescriptor(audioOrSingleRoute); + } + if (videoRoute != null) + { + // Only add routes that have actual switching steps + if (videoRoute.Routes == null || videoRoute.Routes.Count == 0) + { + continue; + } + + RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute); + } + } + } + } + } + } + } + catch (Exception ex) + { + Debug.LogError("Exception mapping routes: {exception}", ex.Message); + Debug.LogDebug(ex, "Stack Trace: "); } } @@ -311,9 +535,9 @@ public static class Extensions /// /// /// - /// The RoutingOutputPort whose link is being checked for a route + /// The RoutingInputPort whose link is being checked for a route /// The RoutingOutputPort whose link is being checked for a route - /// The RoutingOutputPort to use for the route + /// The RoutingOutputPort whose link is being checked for a route /// Prevents Devices from being twice-checked /// This recursive function should not be called with AudioVideo /// Just an informational counter @@ -325,42 +549,53 @@ public static class Extensions { cycle++; + var routeKey = GetRouteKey(source.Key, destination.Key, signalType); + if (_impossibleRoutes.Contains(routeKey)) + { + Debug.LogMessage(LogEventLevel.Verbose, "Route {0} is cached as impossible, skipping", null, routeKey); + return false; + } + Debug.LogMessage(LogEventLevel.Verbose, "GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString()); RoutingInputPort goodInputPort = null; + // Use indexed lookup instead of LINQ query + var allDestinationTieLines = GetTieLinesForDestination(destination.Key); + IEnumerable destinationTieLines; TieLine directTie = null; if (destinationPort == null) { - destinationTieLines = TieLineCollection.Default.Where(t => - t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo)); + destinationTieLines = allDestinationTieLines.Where(t => + t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo); } else { - destinationTieLines = TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && (t.Type.HasFlag(signalType))); + destinationTieLines = allDestinationTieLines.Where(t => + t.DestinationPort.Key == destinationPort.Key && t.Type.HasFlag(signalType)); } // find the TieLine without a port if (destinationPort == null && sourcePort == null) { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key); + directTie = destinationTieLines.FirstOrDefault(t => t.SourcePort.ParentDevice.Key == source.Key); } // find a tieLine to a specific destination port without a specific source port else if (destinationPort != null && sourcePort == null) { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key); + directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key); } // find a tieline to a specific source port without a specific destination port else if (destinationPort == null & sourcePort != null) { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); + directTie = destinationTieLines.FirstOrDefault(t => t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); } // find a tieline to a specific source port and destination port else if (destinationPort != null && sourcePort != null) { - directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); + directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key); } if (directTie != null) // Found a tie directly to the source @@ -415,6 +650,10 @@ public static class Extensions if (goodInputPort == null) { Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key); + + // Cache this as an impossible route + _impossibleRoutes.Add(routeKey); + return false; } diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs index c69285dd..fa68a42d 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs @@ -101,7 +101,7 @@ public class RouteDescriptor { if (route.SwitchingDevice is IRouting switchingDevice) { - if(clearRoute) + if (clearRoute) { try { @@ -138,77 +138,6 @@ public class RouteDescriptor public override string ToString() { var routesText = Routes.Select(r => r.ToString()).ToArray(); - return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); + return $"Route table from {Source.Key} to {Destination.Key} for {SignalType}:\r\n {string.Join("\r\n ", routesText)}"; } -} - -/*/// -/// Represents an collection of individual route steps between Source and Destination -/// -public class RouteDescriptor -{ - public IRoutingInputs Destination { get; private set; } - public IRoutingOutputs Source { get; private set; } - public eRoutingSignalType SignalType { get; private set; } - public List> Routes { get; private set; } - - - public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) - { - Destination = destination; - Source = source; - SignalType = signalType; - Routes = new List>(); - } - - /// - /// Executes all routes described in this collection. Typically called via - /// extension method IRoutingInputs.ReleaseAndMakeRoute() - /// - public void ExecuteRoutes() - { - foreach (var route in Routes) - { - Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString()); - - if (route.SwitchingDevice is IRoutingSinkWithSwitching sink) - { - sink.ExecuteSwitch(route.InputPort.Selector); - continue; - } - - if (route.SwitchingDevice is IRouting switchingDevice) - { - switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType); - - route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType); - - Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } - } - } - - /// - /// Releases all routes in this collection. Typically called via - /// extension method IRoutingInputs.ReleaseAndMakeRoute() - /// - public void ReleaseRoutes() - { - foreach (var route in Routes) - { - if (route.SwitchingDevice is IRouting) - { - // Pull the route from the port. Whatever is watching the output's in use tracker is - // responsible for responding appropriately. - route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType); - Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } - } - } - - public override string ToString() - { - var routesText = Routes.Select(r => r.ToString()).ToArray(); - return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); - } -}*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs index 81f3aa6a..9cf61f77 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs @@ -11,6 +11,9 @@ namespace PepperDash.Essentials.Core; /// public class RouteDescriptorCollection { + /// + /// The static default collection of RouteDescriptors. This is typically used for global routing management across the system, but additional collections could be used for specific purposes if desired. + /// public static RouteDescriptorCollection DefaultCollection { get @@ -24,6 +27,12 @@ public class RouteDescriptorCollection private readonly List RouteDescriptors = new List(); + /// + /// Gets an enumerable collection of all RouteDescriptors in this collection. + /// + public IEnumerable Descriptors => RouteDescriptors.AsReadOnly(); + + /// /// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the /// destination exists already, it will not be added - in order to preserve @@ -37,13 +46,29 @@ public class RouteDescriptorCollection return; } - if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination) - && RouteDescriptors.Any(t => t.Destination == descriptor.Destination && t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key)) + // Check if a route already exists with the same source, destination, input port, AND signal type + var existingRoute = RouteDescriptors.FirstOrDefault(t => + t.Source == descriptor.Source && + t.Destination == descriptor.Destination && + t.SignalType == descriptor.SignalType && + ((t.InputPort == null && descriptor.InputPort == null) || + (t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key))); + + if (existingRoute != null) { - Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination, - "Route to [{0}] already exists in global routes table", descriptor?.Source?.Key); + Debug.LogMessage(LogEventLevel.Information, descriptor.Destination, + "Route from {0} to {1}:{2} ({3}) already exists in this collection", + descriptor?.Source?.Key, + descriptor?.Destination?.Key, + descriptor?.InputPort?.Key ?? "auto", + descriptor?.SignalType); return; } + Debug.LogMessage(LogEventLevel.Verbose, "Adding route descriptor: {0} -> {1}:{2} ({3})", + descriptor?.Source?.Key, + descriptor?.Destination?.Key, + descriptor?.InputPort?.Key ?? "auto", + descriptor?.SignalType); RouteDescriptors.Add(descriptor); } @@ -58,6 +83,12 @@ public class RouteDescriptorCollection return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination); } + /// + /// Gets the route descriptor for a specific destination and input port + /// + /// The destination device + /// The input port key + /// The matching RouteDescriptor or null if not found public RouteDescriptor GetRouteDescriptorForDestinationAndInputPort(IRoutingInputs destination, string inputPortKey) { Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); @@ -68,11 +99,14 @@ public class RouteDescriptorCollection /// Returns the RouteDescriptor for a given destination AND removes it from collection. /// Returns null if no route with the provided destination exists. /// + /// The destination device + /// The input port key (optional) + /// The matching RouteDescriptor or null if not found public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination, string inputPortKey = "") { Debug.LogMessage(LogEventLevel.Information, "Removing route descriptor for '{destination}':'{inputPortKey}'", destination.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey); - var descr = string.IsNullOrEmpty(inputPortKey) + var descr = string.IsNullOrEmpty(inputPortKey) ? GetRouteDescriptorForDestination(destination) : GetRouteDescriptorForDestinationAndInputPort(destination, inputPortKey); if (descr != null) @@ -82,67 +116,4 @@ public class RouteDescriptorCollection return descr; } -} - -/*/// -/// A collection of RouteDescriptors - typically the static DefaultCollection is used -/// -public class RouteDescriptorCollection - { - public static RouteDescriptorCollection DefaultCollection - { - get - { - if (_DefaultCollection == null) - _DefaultCollection = new RouteDescriptorCollection(); - return _DefaultCollection; - } - } - private static RouteDescriptorCollection _DefaultCollection; - - private readonly List RouteDescriptors = new List(); - - /// - /// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the - /// destination exists already, it will not be added - in order to preserve - /// proper route releasing. - /// - /// - /// - /// AddRouteDescriptor method - /// - public void AddRouteDescriptor(RouteDescriptor descriptor) - { - if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination)) - { - Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination, - "Route to [{0}] already exists in global routes table", descriptor.Source.Key); - return; - } - RouteDescriptors.Add(descriptor); - } - - /// - /// Gets the RouteDescriptor for a destination - /// - /// null if no RouteDescriptor for a destination exists - /// - /// GetRouteDescriptorForDestination method - /// - public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs destination) - { - return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination); - } - - /// - /// Returns the RouteDescriptor for a given destination AND removes it from collection. - /// Returns null if no route with the provided destination exists. - /// - public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination) - { - var descr = GetRouteDescriptorForDestination(destination); - if (descr != null) - RouteDescriptors.Remove(descr); - return descr; - } - }*/ \ No newline at end of file +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs index 03759018..227e6d5b 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs @@ -1,54 +1,54 @@ -namespace PepperDash.Essentials.Core; +namespace PepperDash.Essentials.Core +{ + /// + /// Represents a RouteSwitchDescriptor + /// + public class RouteSwitchDescriptor + { + /// + /// Gets or sets the SwitchingDevice + /// + public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } } + /// + /// The output port being switched from (relevant for matrix switchers). Null for sink devices. + /// + public RoutingOutputPort OutputPort { get; set; } + /// + /// The input port being switched to. + /// + public RoutingInputPort InputPort { get; set; } -/// -/// Represents a single switching step within a larger route, detailing the switching device, input port, and optionally the output port. -/// -public class RouteSwitchDescriptor - { - /// - /// Gets or sets the SwitchingDevice - /// - public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } } - /// - /// The output port being switched from (relevant for matrix switchers). Null for sink devices. - /// - public RoutingOutputPort OutputPort { get; set; } - /// - /// The input port being switched to. - /// - public RoutingInputPort InputPort { get; set; } + /// + /// Initializes a new instance of the class for sink devices (no output port). + /// + /// The input port being switched to. + public RouteSwitchDescriptor(RoutingInputPort inputPort) + { + InputPort = inputPort; + } - /// - /// Initializes a new instance of the class for sink devices (no output port). - /// - /// The input port being switched to. - public RouteSwitchDescriptor(RoutingInputPort inputPort) - { - InputPort = inputPort; - } - - /// - /// Initializes a new instance of the class for matrix switchers. - /// - /// The output port being switched from. - /// The input port being switched to. - public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort) - { - InputPort = inputPort; - OutputPort = outputPort; - } - - /// - /// Returns a string representation of the route switch descriptor. - /// - /// A string describing the switch operation. - /// - public override string ToString() - { - if (SwitchingDevice is IRouting) - return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}"; - else - return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}"; - } - } + /// + /// Initializes a new instance of the class for matrix switchers. + /// + /// The output port being switched from. + /// The input port being switched to. + public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort) + { + InputPort = inputPort; + OutputPort = outputPort; + } + /// + /// Returns a string representation of the route switch descriptor. + /// + /// A string describing the switch operation. + /// + public override string ToString() + { + if (SwitchingDevice is IRouting) + return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}"; + else + return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}"; + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 69f41d64..73401600 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -1,248 +1,562 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Timers; using PepperDash.Core; using PepperDash.Essentials.Core.Config; -namespace PepperDash.Essentials.Core.Routing; - -/// -/// Manages routing feedback by subscribing to route changes on midpoint and sink devices, -/// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices. -/// -public class RoutingFeedbackManager : EssentialsDevice +namespace PepperDash.Essentials.Core.Routing { /// - /// Initializes a new instance of the class. + /// Manages routing feedback by subscribing to route changes on midpoint and sink devices, + /// tracing the route back to the original source, and updating the CurrentSourceInfo on sink devices. /// - /// The unique key for this manager device. - /// The name of this manager device. - public RoutingFeedbackManager(string key, string name) : base(key, name) + public class RoutingFeedbackManager : EssentialsDevice { - AddPreActivationAction(SubscribeForMidpointFeedback); - AddPreActivationAction(SubscribeForSinkFeedback); - } + /// + /// Maps midpoint device keys to the set of sink device keys that are downstream + /// + private Dictionary> midpointToSinksMap; + /// + /// Debounce timers for each sink device to prevent rapid successive updates + /// + private readonly Dictionary updateTimers = new Dictionary(); - /// - /// Subscribes to the RouteChanged event on all devices implementing . - /// - private void SubscribeForMidpointFeedback() - { - var midpointDevices = DeviceManager.AllDevices.OfType(); + /// + /// Debounce delay in milliseconds + /// + private const long DEBOUNCE_MS = 500; - foreach (var device in midpointDevices) + /// + /// Initializes a new instance of the class. + /// + /// The unique key for this manager device. + /// The name of this manager device. + public RoutingFeedbackManager(string key, string name) + : base(key, name) { - device.RouteChanged += HandleMidpointUpdate; + AddPreActivationAction(BuildMidpointSinkMap); + AddPreActivationAction(SubscribeForMidpointFeedback); + AddPreActivationAction(SubscribeForSinkFeedback); } - } - /// - /// Subscribes to the InputChanged event on all devices implementing . - /// - private void SubscribeForSinkFeedback() - { - var sinkDevices = DeviceManager.AllDevices.OfType(); - - foreach (var device in sinkDevices) + /// + /// Builds a map of which sink devices are downstream of each midpoint device + /// for performance optimization in HandleMidpointUpdate + /// + private void BuildMidpointSinkMap() { - device.InputChanged += HandleSinkUpdate; - } - } + midpointToSinksMap = new Dictionary>(); - /// - /// Handles the RouteChanged event from a midpoint device. - /// Triggers an update for all sink devices. - /// - /// The midpoint device that reported a route change. - /// The descriptor of the new route. - private void HandleMidpointUpdate(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute) - { - try - { - var devices = DeviceManager.AllDevices.OfType(); + var sinks = DeviceManager.AllDevices.OfType(); + var midpoints = DeviceManager.AllDevices.OfType(); - foreach (var device in devices) + foreach (var sink in sinks) { - UpdateDestination(device, device.CurrentInputPort); + if (sink.CurrentInputPort == null) + continue; + + // Find all upstream midpoints for this sink + var upstreamMidpoints = GetUpstreamMidpoints(sink); + + foreach (var midpointKey in upstreamMidpoints) + { + if (!midpointToSinksMap.ContainsKey(midpointKey)) + midpointToSinksMap[midpointKey] = new HashSet(); + + midpointToSinksMap[midpointKey].Add(sink.Key); + } } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error handling midpoint update from {midpointKey}:{Exception}", this, midpoint.Key, ex); - } - } - /// - /// Handles the InputChanged event from a sink device. - /// Triggers an update for the specific sink device. - /// - /// The sink device that reported an input change. - /// The new input port selected on the sink device. - private void HandleSinkUpdate(IRoutingSinkWithSwitching sender, RoutingInputPort currentInputPort) - { - try - { - UpdateDestination(sender, currentInputPort); - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error handling Sink update from {senderKey}:{Exception}", this, sender.Key, ex); - } - } - - /// - /// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device - /// based on its currently selected input port by tracing the route back through tie lines. - /// - /// The destination sink device to update. - /// The currently selected input port on the destination device. - private void UpdateDestination(IRoutingSink destination, RoutingInputPort inputPort) - { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this,destination?.Key, inputPort?.Key); - - if (inputPort == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Destination {destination} has not reported an input port yet", this, destination.Key); - return; + Debug.LogMessage( + Serilog.Events.LogEventLevel.Information, + "Built midpoint-to-sink map with {count} midpoints", + this, + midpointToSinksMap.Count + ); } - TieLine firstTieLine; - try + /// + /// Gets all upstream midpoint device keys for a given sink + /// + private HashSet GetUpstreamMidpoints(IRoutingSinkWithSwitchingWithInputPort sink) { - var tieLines = TieLineCollection.Default; + var result = new HashSet(); + var visited = new HashSet(); - firstTieLine = tieLines.FirstOrDefault(tl => tl.DestinationPort.Key == inputPort.Key && tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key); + if (sink.CurrentInputPort == null) + return result; - if (firstTieLine == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No tieline found for inputPort {inputPort}. Clearing current source", this, inputPort); + var tieLine = TieLineCollection.Default.FirstOrDefault(tl => + tl.DestinationPort.Key == sink.CurrentInputPort.Key && + tl.DestinationPort.ParentDevice.Key == sink.CurrentInputPort.ParentDevice.Key); + + if (tieLine == null) + return result; + + TraceUpstreamMidpoints(tieLine, result, visited); + return result; + } + + /// + /// Recursively traces upstream to find all midpoint devices + /// + private void TraceUpstreamMidpoints(TieLine tieLine, HashSet midpoints, HashSet visited) + { + if (tieLine == null || visited.Contains(tieLine.SourcePort.ParentDevice.Key)) return; - } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex); - return; - } - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Getting source for first TieLine {tieLine}", this, firstTieLine); - - - TieLine sourceTieLine; - try - { - sourceTieLine = GetRootTieLine(firstTieLine); - - if (sourceTieLine == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route found to source for inputPort {inputPort}. Clearing current source", this, inputPort); - - - destination.SetCurrentSource(sourceTieLine.Type, null); - - - return; - } - } - catch (Exception ex) - { - Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex); - return; - } - - if (sourceTieLine.SourcePort.ParentDevice == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No source found for device {key}. Creating transient source for {destination}", this, sourceTieLine.SourcePort.ParentDevice.Key, destination); - - destination.SetCurrentSource(sourceTieLine.Type, null); - - return; - } - - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Got Source {@source} with key {sourceKey}", this, source, sourceKey); - - if (sourceTieLine.SourcePort.ParentDevice is IRoutingSource sourceDevice) - { - destination.SetCurrentSource(sourceTieLine.Type, sourceDevice); - } - } - - /// - /// Recursively traces a route back from a given tie line to find the root source tie line. - /// It navigates through midpoint devices () by checking their current routes. - /// - /// The starting tie line (typically connected to a sink or midpoint). - /// The connected to the original source device, or null if the source cannot be determined. - private TieLine GetRootTieLine(TieLine tieLine) - { - TieLine nextTieLine = null; - try - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine); + visited.Add(tieLine.SourcePort.ParentDevice.Key); if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint) { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint); + midpoints.Add(midpoint.Key); - if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0) + // Find upstream TieLines connected to this midpoint's inputs + var midpointInputs = (midpoint as IRoutingInputs)?.InputPorts; + if (midpointInputs != null) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Midpoint {midpointKey} has no routes", this, midpoint.Key); - return null; + foreach (var inputPort in midpointInputs) + { + var upstreamTieLine = TieLineCollection.Default.FirstOrDefault(tl => + tl.DestinationPort.Key == inputPort.Key && + tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key); + + if (upstreamTieLine != null) + TraceUpstreamMidpoints(upstreamTieLine, midpoints, visited); + } } - - var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route => - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine); - - return route.OutputPort != null && route.InputPort != null && route.OutputPort?.Key == tieLine.SourcePort.Key && route.OutputPort?.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key; - }); - - if (currentRoute == null) - { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No route through midpoint {midpoint} for outputPort {outputPort}", this, midpoint.Key, tieLine.SourcePort); - return null; - } - - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint); - - nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key); - return tl.DestinationPort.Key == currentRoute.InputPort.Key && tl.DestinationPort.ParentDevice.Key == currentRoute.InputPort.ParentDevice.Key; - }); - - if (nextTieLine != null) - { - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found next tieLine {tieLine}. Walking the chain", this, nextTieLine); - return GetRootTieLine(nextTieLine); - } - - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root tieLine {tieLine}", this,nextTieLine); - return nextTieLine; - } - - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLIne Source Device {sourceDeviceKey} is IRoutingSource: {isIRoutingSource}", this, tieLine.SourcePort.ParentDevice.Key, tieLine.SourcePort.ParentDevice is IRoutingSource); - //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source Device interfaces: {typeFullName}:{interfaces}", this, tieLine.SourcePort.ParentDevice.GetType().FullName, tieLine.SourcePort.ParentDevice.GetType().GetInterfaces().Select(i => i.Name)); - - if (tieLine.SourcePort.ParentDevice is IRoutingSource || tieLine.SourcePort.ParentDevice is IRoutingOutputs) //end of the chain - { - // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root: {tieLine}", this, tieLine); - return tieLine; - } - - nextTieLine = TieLineCollection.Default.FirstOrDefault(tl => tl.DestinationPort.Key == tieLine.SourcePort.Key && tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key); - - if (nextTieLine != null) - { - return GetRootTieLine(nextTieLine); } } - catch (Exception ex) + + /// + /// Subscribes to the RouteChanged event on all devices implementing . + /// + private void SubscribeForMidpointFeedback() { - Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex); - return null; + var midpointDevices = DeviceManager.AllDevices.OfType(); + + foreach (var device in midpointDevices) + { + device.RouteChanged += HandleMidpointUpdate; + } } - return null; + /// + /// Subscribes to the InputChanged event on all devices implementing . + /// + private void SubscribeForSinkFeedback() + { + var sinkDevices = + DeviceManager.AllDevices.OfType(); + + foreach (var device in sinkDevices) + { + device.InputChanged += HandleSinkUpdate; + } + } + + /// + /// Handles the RouteChanged event from a midpoint device. + /// Only triggers updates for sink devices that are downstream of this midpoint. + /// + /// The midpoint device that reported a route change. + /// The descriptor of the new route. + private void HandleMidpointUpdate( + IRoutingWithFeedback midpoint, + RouteSwitchDescriptor newRoute + ) + { + try + { + // Only update affected sinks (performance optimization) + if (midpointToSinksMap != null && midpointToSinksMap.TryGetValue(midpoint.Key, out var affectedSinkKeys)) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Midpoint {midpoint} changed, updating {count} downstream sinks", + this, + midpoint.Key, + affectedSinkKeys.Count + ); + + foreach (var sinkKey in affectedSinkKeys) + { + if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithSwitchingWithInputPort sink) + { + UpdateDestination(sink, sink.CurrentInputPort); + } + } + } + else + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Midpoint {midpoint} changed but has no downstream sinks in map", + this, + midpoint.Key + ); + } + } + catch (Exception ex) + { + Debug.LogMessage( + ex, + "Error handling midpoint update from {midpointKey}:{Exception}", + this, + midpoint.Key, + ex + ); + } + } + + /// + /// Handles the InputChanged event from a sink device. + /// Triggers an update for the specific sink device. + /// + /// The sink device that reported an input change. + /// The new input port selected on the sink device. + private void HandleSinkUpdate( + IRoutingSinkWithSwitching sender, + RoutingInputPort currentInputPort + ) + { + try + { + UpdateDestination(sender, currentInputPort); + } + catch (Exception ex) + { + Debug.LogMessage( + ex, + "Error handling Sink update from {senderKey}:{Exception}", + this, + sender.Key, + ex + ); + } + } + + /// + /// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device + /// based on its currently selected input port by tracing the route back through tie lines. + /// Uses debouncing to prevent rapid successive updates. + /// + /// The destination sink device to update. + /// The currently selected input port on the destination device. + private void UpdateDestination( + IRoutingSinkWithSwitching destination, + RoutingInputPort inputPort + ) + { + if (destination == null) + return; + + var key = destination.Key; + + // Cancel existing timer for this sink + if (updateTimers.TryGetValue(key, out var existingTimer)) + { + existingTimer.Stop(); + existingTimer.Dispose(); + } + + // Start new debounced timer + var timer = new Timer(DEBOUNCE_MS) { AutoReset = false }; + timer.Elapsed += (sender, e) => + { + try + { + UpdateDestinationImmediate(destination, inputPort); + } + catch (Exception ex) + { + Debug.LogMessage( + ex, + "Error in debounced update for destination {destinationKey}: {message}", + this, + destination.Key, + ex.Message + ); + } + finally + { + if (updateTimers.ContainsKey(key)) + { + updateTimers[key]?.Dispose(); + updateTimers.Remove(key); + } + } + }; + timer.Start(); + updateTimers[key] = timer; + } + + /// + /// Immediately updates the CurrentSourceInfo for a destination device. + /// Called after debounce delay. + /// + private void UpdateDestinationImmediate( + IRoutingSinkWithSwitching destination, + RoutingInputPort inputPort + ) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Updating destination {destination} with inputPort {inputPort}", + this, + destination?.Key, + inputPort?.Key + ); + + if (inputPort == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Destination {destination} has not reported an input port yet", + this, + destination.Key + ); + return; + } + + TieLine firstTieLine; + try + { + var tieLines = TieLineCollection.Default; + + firstTieLine = tieLines.FirstOrDefault(tl => + tl.DestinationPort.Key == inputPort.Key + && tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key + ); + + if (firstTieLine == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No tieline found for inputPort {inputPort}. Clearing current source", + this, + inputPort + ); + + var tempSourceListItem = new SourceListItem + { + SourceKey = "$transient", + Name = inputPort.Key, + }; + + return; + } + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Error getting first tieline: {Exception}", this, ex); + return; + } + + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Getting source for first TieLine {tieLine}", this, firstTieLine); + + TieLine sourceTieLine; + try + { + sourceTieLine = GetRootTieLine(firstTieLine); + + if (sourceTieLine == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No route found to source for inputPort {inputPort}. Clearing current source", + this, + inputPort + ); + + var tempSourceListItem = new SourceListItem + { + SourceKey = "$transient", + Name = "None", + }; + + return; + } + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex); + return; + } + + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root TieLine {tieLine}", this, sourceTieLine); + + // Does not handle combinable scenarios or other scenarios where a display might be part of multiple rooms yet. + var room = DeviceManager + .AllDevices.OfType() + .FirstOrDefault( + (r) => + { + + if (r is IHasDefaultDisplay roomDefaultDisplay) + { + return roomDefaultDisplay.DefaultDisplay.Key == destination.Key; + } + + return false; + } + ); + + if (room == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No room found for display {destination}", + this, + destination.Key + ); + return; + } + + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found room {room} for destination {destination}", this, room.Key, destination.Key); + + var sourceList = ConfigReader.ConfigObject.GetSourceListForKey(room.SourceListKey); + + if (sourceList == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}", + this, + room.SourceListKey, + sourceTieLine + ); + return; + } + + // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found sourceList for room {room}", this, room.Key); + + var sourceListItem = sourceList.FirstOrDefault(sli => + { + //// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, + // "SourceListItem {sourceListItem}:{sourceKey} tieLine sourceport device key {sourcePortDeviceKey}", + // this, + // sli.Key, + // sli.Value.SourceKey, + // sourceTieLine.SourcePort.ParentDevice.Key); + + return sli.Value.SourceKey.Equals( + sourceTieLine.SourcePort.ParentDevice.Key, + StringComparison.InvariantCultureIgnoreCase + ); + }); + + var source = sourceListItem.Value; + var sourceKey = sourceListItem.Key; + + if (source == null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No source found for device {key}. Creating transient source for {destination}", + this, + sourceTieLine.SourcePort.ParentDevice.Key, + destination + ); + + + return; + } + + } + + /// + /// Traces a route back from a given tie line to find the root source tie line. + /// Leverages the existing Extensions.GetRouteToSource method with loop protection. + /// + /// The starting tie line (typically connected to a sink or midpoint). + /// The connected to the original source device, or null if the source cannot be determined. + private TieLine GetRootTieLine(TieLine tieLine) + { + try + { + if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink)) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "TieLine destination {device} is not IRoutingInputs", + this, + tieLine.DestinationPort.ParentDevice.Key + ); + return null; + } + + // Get all potential sources (devices that only have outputs, not inputs+outputs) + var sources = DeviceManager.AllDevices + .OfType() + .Where(s => !(s is IRoutingInputsOutputs)); + + // Try each signal type that this TieLine supports + var signalTypes = new[] + { + eRoutingSignalType.Audio, + eRoutingSignalType.Video, + eRoutingSignalType.AudioVideo, + }; + + foreach (var signalType in signalTypes) + { + if (!tieLine.Type.HasFlag(signalType)) + continue; + + foreach (var source in sources) + { + // Use the optimized route discovery with loop protection + var (route, _) = sink.GetRouteToSource( + source, + signalType, + tieLine.DestinationPort, + null + ); + + if (route != null && route.Routes != null && route.Routes.Count > 0) + { + // Found a valid route - return the source TieLine + var sourceTieLine = TieLineCollection.Default.FirstOrDefault(tl => + tl.SourcePort.ParentDevice.Key == source.Key && + tl.Type.HasFlag(signalType)); + + if (sourceTieLine != null) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "Found route from {source} to {sink} with {count} hops", + this, + source.Key, + sink.Key, + route.Routes.Count + ); + return sourceTieLine; + } + } + } + } + + Debug.LogMessage( + Serilog.Events.LogEventLevel.Debug, + "No route found to any source from {sink}", + this, + sink.Key + ); + return null; + } + catch (Exception ex) + { + Debug.LogMessage( + ex, + "Error getting root tieLine: {Exception}", + this, + ex + ); + return null; + } + } } -} +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs b/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs index e3d679da..f3c346dc 100644 --- a/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs +++ b/src/PepperDash.Essentials.Core/Routing/eRoutingSignalType.cs @@ -27,16 +27,7 @@ namespace PepperDash.Essentials.Core /// /// Control signal type /// - UsbOutput = 8, + Usb = 8, - /// - /// Control signal type - /// - UsbInput = 16, - - /// - /// Secondary audio signal type - /// - SecondaryAudio = 32 } } diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs index 8a66431b..f661a7ff 100644 --- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs +++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs @@ -179,6 +179,11 @@ public class EssentialsWebApi : EssentialsDevice Name = "Get Routing Ports for a device", RouteHandler = new GetRoutingPortsHandler() }, + new HttpCwsRoute("routingDevicesAndTieLines") + { + Name = "Get Routing Devices and TieLines", + RouteHandler = new GetRoutingDevicesAndTieLinesHandler() + }, }; AddRoute(routes); diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs new file mode 100644 index 00000000..72a6757f --- /dev/null +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs @@ -0,0 +1,241 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Web.RequestHandlers; + +namespace PepperDash.Essentials.Core.Web.RequestHandlers +{ + /// + /// Handles HTTP requests to retrieve routing devices and tielines information + /// + public class GetRoutingDevicesAndTieLinesHandler : WebApiBaseRequestHandler + { + + /// + /// Initializes a new instance of the class. + /// + public GetRoutingDevicesAndTieLinesHandler() : base(true) { } + + /// + /// Handles the GET request to retrieve routing devices and tielines information + /// + /// + protected override void HandleGet(HttpCwsContext context) + { + var devices = new List(); + + // Get all devices from DeviceManager + foreach (var device in DeviceManager.AllDevices) + { + var deviceInfo = new RoutingDeviceInfo + { + Key = device.Key, + Name = (device as IKeyName)?.Name ?? device.Key + }; + + // Check if device implements IRoutingInputs + if (device is IRoutingInputs inputDevice) + { + deviceInfo.HasInputs = true; + deviceInfo.InputPorts = inputDevice.InputPorts.Select(p => new PortInfo + { + Key = p.Key, + SignalType = p.Type.ToString(), + ConnectionType = p.ConnectionType.ToString(), + IsInternal = p.IsInternal + }).ToList(); + } + + // Check if device implements IRoutingOutputs + if (device is IRoutingOutputs outputDevice) + { + deviceInfo.HasOutputs = true; + deviceInfo.OutputPorts = outputDevice.OutputPorts.Select(p => new PortInfo + { + Key = p.Key, + SignalType = p.Type.ToString(), + ConnectionType = p.ConnectionType.ToString(), + IsInternal = p.IsInternal + }).ToList(); + } + + // Check if device implements IRoutingInputsOutputs + if (device is IRoutingInputsOutputs) + { + deviceInfo.HasInputsAndOutputs = true; + } + + // Only include devices that have routing capabilities + if (deviceInfo.HasInputs || deviceInfo.HasOutputs) + { + devices.Add(deviceInfo); + } + } + + // Get all tielines + var tielines = TieLineCollection.Default.Select(tl => new TieLineInfo + { + SourceDeviceKey = tl.SourcePort.ParentDevice.Key, + SourcePortKey = tl.SourcePort.Key, + DestinationDeviceKey = tl.DestinationPort.ParentDevice.Key, + DestinationPortKey = tl.DestinationPort.Key, + SignalType = tl.Type.ToString(), + IsInternal = tl.IsInternal + }).ToList(); + + var response = new RoutingSystemInfo + { + Devices = devices, + TieLines = tielines + }; + + var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented); + + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = Encoding.UTF8; + context.Response.Write(jsonResponse, false); + context.Response.End(); + } + } + + /// + /// Represents the complete routing system information including devices and tielines + /// + public class RoutingSystemInfo + { + + /// + /// Gets or sets the list of routing devices in the system, including their ports information + /// + [JsonProperty("devices")] + public List Devices { get; set; } + + + /// + /// Gets or sets the list of tielines in the system, including source/destination device and port information + /// + [JsonProperty("tieLines")] + public List TieLines { get; set; } + } + + /// + /// Represents a routing device with its ports information + /// + public class RoutingDeviceInfo : IKeyName + { + + /// + [JsonProperty("key")] + public string Key { get; set; } + + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// Gets or sets a value indicating whether the device has routing input ports + /// + [JsonProperty("hasInputs")] + public bool HasInputs { get; set; } + + /// + /// Gets or sets a value indicating whether the device has routing output ports + /// + [JsonProperty("hasOutputs")] + public bool HasOutputs { get; set; } + + /// + /// Gets or sets a value indicating whether the device has both routing inputs and outputs (e.g., matrix switcher) + /// + [JsonProperty("hasInputsAndOutputs")] + public bool HasInputsAndOutputs { get; set; } + + /// + /// Gets or sets the list of input ports for the device, if applicable. Null if the device does not have routing inputs. + /// + [JsonProperty("inputPorts", NullValueHandling = NullValueHandling.Ignore)] + public List InputPorts { get; set; } + + /// + /// Gets or sets the list of output ports for the device, if applicable. Null if the device does not have routing outputs. + /// + [JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)] + public List OutputPorts { get; set; } + } + + /// + /// Represents a routing port with its properties + /// + public class PortInfo : IKeyed + { + /// + [JsonProperty("key")] + public string Key { get; set; } + + /// + /// Gets or sets the signal type of the port (e.g., AudioVideo, Audio, Video, etc.) + /// + [JsonProperty("signalType")] + public string SignalType { get; set; } + + /// + /// Gets or sets the connection type of the port (e.g., Hdmi, Dvi, Vga, etc.) + /// + [JsonProperty("connectionType")] + public string ConnectionType { get; set; } + + /// + /// Gets or sets a value indicating whether the port is internal + /// + [JsonProperty("isInternal")] + public bool IsInternal { get; set; } + } + + /// + /// Represents a tieline connection between two ports + /// + public class TieLineInfo + { + /// + /// Gets or sets the key of the source device for the tieline connection + /// + [JsonProperty("sourceDeviceKey")] + public string SourceDeviceKey { get; set; } + + + /// + /// Gets or sets the key of the source port for the tieline connection + /// + [JsonProperty("sourcePortKey")] + public string SourcePortKey { get; set; } + + /// + /// Gets or sets the key of the destination device for the tieline connection + /// + [JsonProperty("destinationDeviceKey")] + public string DestinationDeviceKey { get; set; } + + /// + /// Gets or sets the key of the destination port for the tieline connection + /// + [JsonProperty("destinationPortKey")] + public string DestinationPortKey { get; set; } + + /// + /// Gets or sets the signal type of the tieline connection (e.g., AudioVideo, Audio, Video, etc.) + /// + [JsonProperty("signalType")] + public string SignalType { get; set; } + + /// + /// Gets or sets a value indicating whether the tieline connection is internal + /// + [JsonProperty("isInternal")] + public bool IsInternal { get; set; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs index 46507a98..7e449564 100644 --- a/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs +++ b/src/PepperDash.Essentials.Devices.Common/Generic/GenericSink.cs @@ -32,7 +32,7 @@ public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputP { InputPorts = new RoutingPortCollection(); - var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this); + var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); InputPorts.Add(inputPort); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs similarity index 86% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs index e2c1473d..ae00d78a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CurrentSourcesMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICurrentSourcesMessenger.cs @@ -12,17 +12,17 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Represents a IHasCurrentSourceInfoMessenger /// - public class CurrentSourcesMessenger : MessengerBase + public class ICurrentSourcesMessenger : MessengerBase { private readonly ICurrentSources sourceDevice; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The key. /// The message path. /// The device. - public CurrentSourcesMessenger(string key, string messagePath, ICurrentSources device) : base(key, messagePath, device as IKeyName) + public ICurrentSourcesMessenger(string key, string messagePath, ICurrentSources device) : base(key, messagePath, device as IKeyName) { sourceDevice = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs similarity index 90% rename from src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs index 15e645a6..0bad9a89 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDeviceInfoProviderMessenger.cs @@ -10,19 +10,19 @@ namespace PepperDash.Essentials.AppServer.Messengers /// Facilitates communication of device information by providing mechanisms for status updates and device /// information reporting. /// - /// The class integrates with an The class integrates with an to manage device-specific information. It uses a debounce timer to limit the /// frequency of updates, ensuring efficient communication. The timer is initialized with a 1-second interval and /// is disabled by default. This class also subscribes to device information change events and provides actions for /// reporting full device status and triggering updates. - public class DeviceInfoMessenger : MessengerBase + public class IDeviceInfoProviderMessenger : MessengerBase { private readonly IDeviceInfoProvider _deviceInfoProvider; private readonly Timer debounceTimer; /// - /// Initializes a new instance of the class, which facilitates communication + /// Initializes a new instance of the class, which facilitates communication /// of device information. /// /// The messenger uses a debounce timer to limit the frequency of certain operations. The @@ -30,7 +30,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// A unique identifier for the messenger instance. /// The path used for sending and receiving messages. /// An implementation of that provides device-specific information. - public DeviceInfoMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device) + public IDeviceInfoProviderMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device) { _deviceInfoProvider = device; diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index cba10266..1b0580f6 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -774,7 +774,7 @@ namespace PepperDash.Essentials { this.LogVerbose("Adding CurrentSourcesMessenger for {deviceKey}", device.Key); - var messenger = new CurrentSourcesMessenger($"{device.Key}-currentSources-{Key}", $"/device/{device.Key}", currentSources); + var messenger = new ICurrentSourcesMessenger($"{device.Key}-currentSources-{Key}", $"/device/{device.Key}", currentSources); AddDefaultDeviceMessenger(messenger); @@ -803,7 +803,7 @@ namespace PepperDash.Essentials this.LogVerbose("Adding IHasDeviceInfoMessenger for {deviceKey}", device.Key ); - var messenger = new DeviceInfoMessenger( + var messenger = new IDeviceInfoProviderMessenger( $"{device.Key}-deviceInfo-{Key}", $"/device/{device.Key}", provider diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 52a47273..01859cb2 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -140,6 +140,17 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(ListTieLines, + "listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes", + "Visualizes routes by signal type", + ConsoleAccessLevelEnum.AccessOperator); + + CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes", + "Visualizes current active routes from DefaultCollection", + ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(s => { foreach (var tl in TieLineCollection.Default) @@ -480,6 +491,282 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded."); + Extensions.MapDestinationsToSources(); + + Debug.LogMessage(LogEventLevel.Information, "All Routes Mapped."); + } + + + + /// + /// Visualizes routes in a tree format for better understanding of signal paths + /// + private void ListTieLines(string args) + { + try + { + if (args.Contains("?")) + { + CrestronConsole.ConsoleCommandResponse("Usage: listtielines [signaltype]\r\n"); + CrestronConsole.ConsoleCommandResponse("Signal types: Audio, Video, SecondaryAudio, AudioVideo, UsbInput, UsbOutput\r\n"); + return; + } + + eRoutingSignalType? signalTypeFilter = null; + if (!string.IsNullOrEmpty(args)) + { + eRoutingSignalType parsedType; + if (Enum.TryParse(args.Trim(), true, out parsedType)) + { + signalTypeFilter = parsedType; + } + else + { + CrestronConsole.ConsoleCommandResponse("Invalid signal type: {0}\r\n", args.Trim()); + CrestronConsole.ConsoleCommandResponse("Valid types: Audio, Video, SecondaryAudio, AudioVideo, UsbInput, UsbOutput\r\n"); + return; + } + } + + var tielines = signalTypeFilter.HasValue + ? TieLineCollection.Default.Where(tl => tl.Type.HasFlag(signalTypeFilter.Value)) + : TieLineCollection.Default; + + var count = 0; + foreach (var tl in tielines) + { + CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); + count++; + } + + CrestronConsole.ConsoleCommandResponse("\r\nTotal: {0} tieline{1}{2}", count, count == 1 ? "" : "s", CrestronEnvironment.NewLine); + } + catch (Exception ex) + { + CrestronConsole.ConsoleCommandResponse("Error listing tielines: {0}\r\n", ex.Message); + } + } + + private void VisualizeRoutes(string args) + { + try + { + if (args.Contains("?")) + { + CrestronConsole.ConsoleCommandResponse("Usage: visualizeroutes [signaltype] [-s source] [-d destination]\r\n"); + CrestronConsole.ConsoleCommandResponse(" signaltype: Audio, Video, AudioVideo, etc.\r\n"); + CrestronConsole.ConsoleCommandResponse(" -s: Filter by source key (partial match)\r\n"); + CrestronConsole.ConsoleCommandResponse(" -d: Filter by destination key (partial match)\r\n"); + return; + } + + ParseRouteFilters(args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter); + + CrestronConsole.ConsoleCommandResponse("\r\n+===========================================================================+\r\n"); + CrestronConsole.ConsoleCommandResponse("| ROUTE VISUALIZATION |\r\n"); + CrestronConsole.ConsoleCommandResponse("+===========================================================================+\r\n\r\n"); + + foreach (var descriptorCollection in Extensions.RouteDescriptors.Where(kv => kv.Value.Descriptors.Count() > 0)) + { + // Filter by signal type if specified + if (signalTypeFilter.HasValue && descriptorCollection.Key != signalTypeFilter.Value) + continue; + + CrestronConsole.ConsoleCommandResponse("\r\n+--- Signal Type: {0} ({1} routes) ---\r\n", + descriptorCollection.Key, + descriptorCollection.Value.Descriptors.Count()); + + foreach (var descriptor in descriptorCollection.Value.Descriptors) + { + // Filter by source/dest if specified + if (sourceFilter != null && !descriptor.Source.Key.ToLower().Contains(sourceFilter)) + continue; + if (destFilter != null && !descriptor.Destination.Key.ToLower().Contains(destFilter)) + continue; + + VisualizeRouteDescriptor(descriptor); + } + } + + CrestronConsole.ConsoleCommandResponse("\r\n"); + } + catch (Exception ex) + { + CrestronConsole.ConsoleCommandResponse("Error visualizing routes: {0}\r\n", ex.Message); + } + } + + private void VisualizeCurrentRoutes(string args) + { + try + { + if (args.Contains("?")) + { + CrestronConsole.ConsoleCommandResponse("Usage: visualizecurrentroutes [signaltype] [-s source] [-d destination]\r\n"); + CrestronConsole.ConsoleCommandResponse(" signaltype: Audio, Video, AudioVideo, etc.\r\n"); + CrestronConsole.ConsoleCommandResponse(" -s: Filter by source key (partial match)\r\n"); + CrestronConsole.ConsoleCommandResponse(" -d: Filter by destination key (partial match)\r\n"); + return; + } + + ParseRouteFilters(args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter); + + CrestronConsole.ConsoleCommandResponse("\r\n+===========================================================================+\r\n"); + CrestronConsole.ConsoleCommandResponse("| CURRENT ROUTES VISUALIZATION |\r\n"); + CrestronConsole.ConsoleCommandResponse("+===========================================================================+\r\n\r\n"); + + var hasRoutes = false; + + // Get all descriptors from DefaultCollection + var allDescriptors = RouteDescriptorCollection.DefaultCollection.Descriptors; + + // Group by signal type + var groupedByType = allDescriptors.GroupBy(d => d.SignalType); + + foreach (var group in groupedByType) + { + var signalType = group.Key; + + // Filter by signal type if specified + if (signalTypeFilter.HasValue && signalType != signalTypeFilter.Value) + continue; + + var filteredDescriptors = group.Where(d => + { + if (sourceFilter != null && !d.Source.Key.ToLower().Contains(sourceFilter)) + return false; + if (destFilter != null && !d.Destination.Key.ToLower().Contains(destFilter)) + return false; + return true; + }).ToList(); + + if (filteredDescriptors.Count == 0) + continue; + + hasRoutes = true; + CrestronConsole.ConsoleCommandResponse("\r\n+--- Signal Type: {0} ({1} routes) ---\r\n", + signalType, + filteredDescriptors.Count); + + foreach (var descriptor in filteredDescriptors) + { + VisualizeRouteDescriptor(descriptor); + } + } + + if (!hasRoutes) + { + CrestronConsole.ConsoleCommandResponse("\r\nNo active routes found in current state.\r\n"); + } + + CrestronConsole.ConsoleCommandResponse("\r\n"); + } + catch (Exception ex) + { + CrestronConsole.ConsoleCommandResponse("Error visualizing current state: {0}\r\n", ex.Message); + } + } + + /// + /// Parses route filter arguments from command line + /// + /// Command line arguments + /// Parsed signal type filter (if any) + /// Parsed source filter (if any) + /// Parsed destination filter (if any) + private void ParseRouteFilters(string args, out eRoutingSignalType? signalTypeFilter, out string sourceFilter, out string destFilter) + { + signalTypeFilter = null; + sourceFilter = null; + destFilter = null; + + if (string.IsNullOrEmpty(args)) + return; + + var parts = args.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + for (int i = 0; i < parts.Length; i++) + { + var part = parts[i]; + + // Check for flags + if (part == "-s" && i + 1 < parts.Length) + { + sourceFilter = parts[++i].ToLower(); + } + else if (part == "-d" && i + 1 < parts.Length) + { + destFilter = parts[++i].ToLower(); + } + // Try to parse as signal type if not a flag and no signal type set yet + else if (!part.StartsWith("-") && !signalTypeFilter.HasValue) + { + if (Enum.TryParse(part, true, out eRoutingSignalType parsedType)) + { + signalTypeFilter = parsedType; + } + } + } + } + + /// + /// Visualizes a single route descriptor in a tree format + /// + private void VisualizeRouteDescriptor(RouteDescriptor descriptor) + { + CrestronConsole.ConsoleCommandResponse("|\r\n"); + CrestronConsole.ConsoleCommandResponse("|-- {0} --> {1}\r\n", + descriptor.Source.Key, + descriptor.Destination.Key); + + if (descriptor.Routes == null || descriptor.Routes.Count == 0) + { + CrestronConsole.ConsoleCommandResponse("| +-- (No switching steps)\r\n"); + return; + } + + for (int i = 0; i < descriptor.Routes.Count; i++) + { + var route = descriptor.Routes[i]; + var isLast = i == descriptor.Routes.Count - 1; + var prefix = isLast ? "+" : "|"; + var continuation = isLast ? " " : "|"; + + if (route.SwitchingDevice != null) + { + CrestronConsole.ConsoleCommandResponse("| {0}-- [{1}] {2}\r\n", + prefix, + route.SwitchingDevice.Key, + GetSwitchDescription(route)); + + // Add visual connection line for non-last items + if (!isLast) + CrestronConsole.ConsoleCommandResponse("| {0} |\r\n", continuation); + } + else + { + CrestronConsole.ConsoleCommandResponse("| {0}-- {1}\r\n", prefix, route.ToString()); + } + } + } + + /// + /// Gets a readable description of the switching operation + /// + private string GetSwitchDescription(RouteSwitchDescriptor route) + { + if (route.OutputPort != null && route.InputPort != null) + { + return string.Format("{0} -> {1}", route.OutputPort.Key, route.InputPort.Key); + } + else if (route.InputPort != null) + { + return string.Format("-> {0}", route.InputPort.Key); + } + else + { + return "(passthrough)"; + } } ///