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)";
+ }
}
///