From fb8216beedb94990f76495358d1a9c8917c9362a Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 14 Jan 2026 10:00:48 -0600 Subject: [PATCH] feat: map routes/tielines at startup and new console commands * visualizeroutes allows visualizing configured routes based on tielines and signal type * can be filtered by source key, destination key, and type, along with partial matches for source & destination keys * visualizecurrentroutes visualizes what Essentials says is currently routed by type * uses same filtering as visualizeroutes * improvements to how the routing algorithm works --- .../Routing/Extensions.cs | 282 ++++++++++++++++- .../Routing/RouteDescriptor.cs | 95 +----- .../Routing/RouteDescriptorCollection.cs | 112 +++---- .../Routing/RouteSwitchDescriptor.cs | 87 ++---- src/PepperDash.Essentials/ControlSystem.cs | 293 +++++++++++++++++- 5 files changed, 621 insertions(+), 248 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index 5d09176f..72dd8d07 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -18,6 +18,20 @@ namespace PepperDash.Essentials.Core /// public static class Extensions { + + /// + /// A collection of RouteDescriptors for each signal type. + /// + public static readonly Dictionary RouteDescriptors = new Dictionary() + { + { eRoutingSignalType.Audio, new RouteDescriptorCollection() }, + { eRoutingSignalType.Video, new RouteDescriptorCollection() }, + { eRoutingSignalType.SecondaryAudio, new RouteDescriptorCollection() }, + { eRoutingSignalType.AudioVideo, new RouteDescriptorCollection() }, + { eRoutingSignalType.UsbInput, new RouteDescriptorCollection() }, + { eRoutingSignalType.UsbOutput, new RouteDescriptorCollection() } + }; + /// /// Stores pending route requests, keyed by the destination device key. /// Used primarily to handle routing requests while a device is cooling down. @@ -29,6 +43,105 @@ namespace PepperDash.Essentials.Core /// 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 @@ -173,8 +286,9 @@ namespace PepperDash.Essentials.Core 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); } /// @@ -245,6 +359,90 @@ namespace PepperDash.Essentials.Core routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest)); } + /// + /// 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.SecondaryAudio)) + { + RouteDescriptors[eRoutingSignalType.SecondaryAudio].AddRouteDescriptor(audioOrSingleRoute); + } + if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.UsbInput)) + { + RouteDescriptors[eRoutingSignalType.UsbInput].AddRouteDescriptor(audioOrSingleRoute); + } + if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.UsbOutput)) + { + RouteDescriptors[eRoutingSignalType.UsbOutput].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: "); + } + } + /// /// Executes the actual routing based on a . /// Finds the route path, adds it to the collection, and executes the switches. @@ -257,7 +455,51 @@ namespace PepperDash.Essentials.Core 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 + var signalTypeToCheck = request.SignalType.HasFlag(eRoutingSignalType.SecondaryAudio) + ? eRoutingSignalType.SecondaryAudio + : request.SignalType; + + if (RouteDescriptors.TryGetValue(signalTypeToCheck, 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; @@ -321,11 +563,13 @@ namespace PepperDash.Essentials.Core /// /// /// - /// The RoutingOutputPort whose link is being checked for a 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 /// The RouteDescriptor being populated as the route is discovered + /// The RoutingOutputPort whose link is being checked for a route + /// The source output port (optional) /// true if source is hit private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, RoutingOutputPort outputPortToUse, List alreadyCheckedDevices, @@ -333,42 +577,54 @@ namespace PepperDash.Essentials.Core { cycle++; + // Check if this route has already been determined to be impossible + 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 @@ -423,6 +679,10 @@ namespace PepperDash.Essentials.Core 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 aab82d38..c35b0afa 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs @@ -95,15 +95,15 @@ namespace PepperDash.Essentials.Core /// Releases the usage tracking for the route and optionally clears the route on the switching devices. /// /// If true, attempts to clear the route on the switching devices (e.g., set input to null/0). - - + + public void ReleaseRoutes(bool clearRoute = false) { foreach (var route in Routes.Where(r => r.SwitchingDevice is IRouting)) { if (route.SwitchingDevice is IRouting switchingDevice) { - if(clearRoute) + if (clearRoute) { try { @@ -137,98 +137,11 @@ namespace PepperDash.Essentials.Core /// Returns a string representation of the route descriptor, including source, destination, and individual route steps. /// /// A string describing the route. - - - public override string ToString() { var routesText = Routes.Select(r => r.ToString()).ToArray(); - return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); + 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 - /// - /// - /// Represents a RouteDescriptor - /// - public class RouteDescriptor - { - /// - /// Gets or sets the Destination - /// - public IRoutingInputs Destination { get; private set; } - /// - /// Gets or sets the Source - /// - public IRoutingOutputs Source { get; private set; } - /// - /// Gets or sets the SignalType - /// - public eRoutingSignalType SignalType { get; private set; } - public List> Routes { get; private set; } - - - public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, eRoutingSignalType signalType) - { - Destination = destination; - Source = source; - SignalType = signalType; - Routes = new List>(); - } - - /// - /// ExecuteRoutes method - /// - 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); - } - } - } - - /// - /// ReleaseRoutes method - /// - public void ReleaseRoutes() - { - foreach (var route in Routes) - { - if (route.SwitchingDevice is IRouting) - { - // Pull the route from the port. Whatever is watching the output's in use tracker is - // responsible for responding appropriately. - route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType); - Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue); - } - } - } - - /// - /// ToString method - /// - /// - public override string ToString() - { - var routesText = Routes.Select(r => r.ToString()).ToArray(); - return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText)); - } - }*/ } \ No newline at end of file diff --git a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs index f389f719..5d8147cb 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptorCollection.cs @@ -1,7 +1,7 @@ -using PepperDash.Core; -using Serilog.Events; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using PepperDash.Core; +using Serilog.Events; namespace PepperDash.Essentials.Core @@ -11,6 +11,9 @@ namespace PepperDash.Essentials.Core /// public class RouteDescriptorCollection { + /// + /// Gets the default collection of RouteDescriptors. + /// public static RouteDescriptorCollection DefaultCollection { get @@ -24,6 +27,11 @@ namespace PepperDash.Essentials.Core 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 +45,29 @@ namespace PepperDash.Essentials.Core 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); } @@ -57,6 +81,12 @@ namespace PepperDash.Essentials.Core 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); @@ -73,7 +103,7 @@ namespace PepperDash.Essentials.Core { 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) @@ -84,70 +114,4 @@ namespace PepperDash.Essentials.Core return descr; } } - - /*/// - /// A collection of RouteDescriptors - typically the static DefaultCollection is used - /// - /// - /// Represents a RouteDescriptorCollection - /// - 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 diff --git a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs index 12aebdcf..227e6d5b 100644 --- a/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs +++ b/src/PepperDash.Essentials.Core/Routing/RouteSwitchDescriptor.cs @@ -4,96 +4,51 @@ /// 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; } - - /// - /// 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")}"; - } - } - - /*/// - /// Represents an individual link for a route - /// - /// - /// Represents a RouteSwitchDescriptor - /// - public class RouteSwitchDescriptor { /// /// Gets or sets the SwitchingDevice /// - public IRoutingInputs SwitchingDevice { get { return InputPort.ParentDevice; } } + public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } } /// - /// Gets or sets the OutputPort + /// The output port being switched from (relevant for matrix switchers). Null for sink devices. /// - public RoutingOutputPort OutputPort { get; set; } + public RoutingOutputPort OutputPort { get; set; } /// - /// Gets or sets the InputPort + /// The input port being switched to. /// - public RoutingInputPort InputPort { get; set; } + public RoutingInputPort InputPort { get; set; } - public RouteSwitchDescriptor(RoutingInputPort 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; } - public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort 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; } /// - /// ToString method + /// Returns a string representation of the route switch descriptor. /// + /// A string describing the switch operation. /// public override string ToString() { if (SwitchingDevice is IRouting) - return string.Format("{0} switches output '{1}' to input '{2}'", SwitchingDevice.Key, OutputPort.Selector, InputPort.Selector); + 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 string.Format("{0} switches to input '{1}'", SwitchingDevice.Key, InputPort.Selector); + 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/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index ef19c955..5fb7771e 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO.Compression; using System.Linq; using System.Reflection; @@ -92,12 +93,16 @@ namespace PepperDash.Essentials CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); - CrestronConsole.AddNewConsoleCommand(s => - { - foreach (var tl in TieLineCollection.Default) - CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine); - }, - "listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(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 => { @@ -443,6 +448,282 @@ namespace PepperDash.Essentials 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)"; + } } ///