diff --git a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs
index c199773ab..f32fe84ed 100644
--- a/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs
+++ b/src/PepperDash.Essentials.Core/Devices/DeviceManager.cs
@@ -436,14 +436,14 @@ public static void GetRoutingPorts(string s)
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 5d09176f8..72dd8d070 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 @@ public static class Extensions
///
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 @@ public static (RouteDescriptor, RouteDescriptor) GetRouteToSource(this IRoutingI
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 @@ private static void ReleaseAndMakeRoute(IRoutingInputs destination, IRoutingOutp
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 @@ private static void RunRouteRequest(RouteRequest request)
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 @@ private static void ReleaseRouteInternal(IRoutingInputs destination, string inpu
///
///
///
- /// 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 @@ private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOu
{
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 @@ private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOu
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 aab82d38e..c35b0afab 100644
--- a/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs
+++ b/src/PepperDash.Essentials.Core/Routing/RouteDescriptor.cs
@@ -95,15 +95,15 @@ public void ExecuteRoutes()
/// 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 @@ public void ReleaseRoutes(bool clearRoute = false)
/// 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 f389f7194..5d8147cb1 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 @@ public static RouteDescriptorCollection DefaultCollection
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 @@ public void AddRouteDescriptor(RouteDescriptor descriptor)
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 @@ public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs destinati
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 @@ public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination, string
{
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 @@ public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs destination, string
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 12aebdcf2..227e6d5b5 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.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs
index e37d2d578..61dfcbabf 100644
--- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs
+++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.Linq;
+using Crestron.SimplSharp;
using PepperDash.Core;
using PepperDash.Essentials.Core.Config;
@@ -11,6 +13,21 @@ namespace PepperDash.Essentials.Core.Routing
///
public class RoutingFeedbackManager : EssentialsDevice
{
+ ///
+ /// 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();
+
+ ///
+ /// Debounce delay in milliseconds
+ ///
+ private const long DEBOUNCE_MS = 500;
+
///
/// Initializes a new instance of the class.
///
@@ -19,10 +36,100 @@ public class RoutingFeedbackManager : EssentialsDevice
public RoutingFeedbackManager(string key, string name)
: base(key, name)
{
+ AddPreActivationAction(BuildMidpointSinkMap);
AddPreActivationAction(SubscribeForMidpointFeedback);
AddPreActivationAction(SubscribeForSinkFeedback);
}
+ ///
+ /// Builds a map of which sink devices are downstream of each midpoint device
+ /// for performance optimization in HandleMidpointUpdate
+ ///
+ private void BuildMidpointSinkMap()
+ {
+ midpointToSinksMap = new Dictionary>();
+
+ var sinks = DeviceManager.AllDevices.OfType();
+ var midpoints = DeviceManager.AllDevices.OfType();
+
+ foreach (var sink in sinks)
+ {
+ 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);
+ }
+ }
+
+ Debug.LogMessage(
+ Serilog.Events.LogEventLevel.Information,
+ "Built midpoint-to-sink map with {count} midpoints",
+ this,
+ midpointToSinksMap.Count
+ );
+ }
+
+ ///
+ /// Gets all upstream midpoint device keys for a given sink
+ ///
+ private HashSet GetUpstreamMidpoints(IRoutingSinkWithSwitchingWithInputPort sink)
+ {
+ var result = new HashSet();
+ var visited = new HashSet();
+
+ if (sink.CurrentInputPort == null)
+ return result;
+
+ 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;
+
+ visited.Add(tieLine.SourcePort.ParentDevice.Key);
+
+ if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
+ {
+ midpoints.Add(midpoint.Key);
+
+ // Find upstream TieLines connected to this midpoint's inputs
+ var midpointInputs = (midpoint as IRoutingInputs)?.InputPorts;
+ if (midpointInputs != 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);
+ }
+ }
+ }
+ }
+
///
/// Subscribes to the RouteChanged event on all devices implementing .
///
@@ -52,7 +159,7 @@ private void SubscribeForSinkFeedback()
///
/// Handles the RouteChanged event from a midpoint device.
- /// Triggers an update for all sink devices.
+ /// 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.
@@ -63,12 +170,33 @@ RouteSwitchDescriptor newRoute
{
try
{
- var devices =
- DeviceManager.AllDevices.OfType();
+ // 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 device in devices)
+ foreach (var sinkKey in affectedSinkKeys)
+ {
+ if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithSwitchingWithInputPort sink)
+ {
+ UpdateDestination(sink, sink.CurrentInputPort);
+ }
+ }
+ }
+ else
{
- UpdateDestination(device, device.CurrentInputPort);
+ Debug.LogMessage(
+ Serilog.Events.LogEventLevel.Debug,
+ "Midpoint {midpoint} changed but has no downstream sinks in map",
+ this,
+ midpoint.Key
+ );
}
}
catch (Exception ex)
@@ -113,6 +241,7 @@ RoutingInputPort currentInputPort
///
/// 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.
@@ -120,6 +249,55 @@ 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
+ updateTimers[key] = new CTimer(_ =>
+ {
+ 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);
+ }
+ }
+ }, null, DEBOUNCE_MS);
+ }
+
+ ///
+ /// 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,
@@ -309,105 +487,98 @@ RoutingInputPort inputPort
}
///
- /// 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.
+ /// 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)
{
- TieLine nextTieLine = null;
try
{
- //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine);
-
- if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
+ if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink))
{
- // Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint);
+ Debug.LogMessage(
+ Serilog.Events.LogEventLevel.Debug,
+ "TieLine destination {device} is not IRoutingInputs",
+ this,
+ tieLine.DestinationPort.ParentDevice.Key
+ );
+ return null;
+ }
- if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0)
- {
- Debug.LogMessage(
- Serilog.Events.LogEventLevel.Debug,
- "Midpoint {midpointKey} has no routes",
- this,
- midpoint.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));
- var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route =>
- {
- //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine);
+ // Try each signal type that this TieLine supports
+ var signalTypes = new[]
+ {
+ eRoutingSignalType.Audio,
+ eRoutingSignalType.Video,
+ eRoutingSignalType.AudioVideo,
+ eRoutingSignalType.SecondaryAudio,
+ eRoutingSignalType.UsbInput,
+ eRoutingSignalType.UsbOutput
+ };
- return route.OutputPort != null
- && route.InputPort != null
- && route.OutputPort?.Key == tieLine.SourcePort.Key
- && route.OutputPort?.ParentDevice.Key
- == tieLine.SourcePort.ParentDevice.Key;
- });
+ foreach (var signalType in signalTypes)
+ {
+ if (!tieLine.Type.HasFlag(signalType))
+ continue;
- if (currentRoute == null)
+ foreach (var source in sources)
{
- Debug.LogMessage(
- Serilog.Events.LogEventLevel.Debug,
- "No route through midpoint {midpoint} for outputPort {outputPort}",
- this,
- midpoint.Key,
- tieLine.SourcePort
+ // Use the optimized route discovery with loop protection
+ var (route, _) = sink.GetRouteToSource(
+ source,
+ signalType,
+ tieLine.DestinationPort,
+ null
);
- 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);
+ 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.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
+ Debug.LogMessage(
+ Serilog.Events.LogEventLevel.Debug,
+ "No route found to any source from {sink}",
+ this,
+ sink.Key
);
-
- if (nextTieLine != null)
- {
- return GetRootTieLine(nextTieLine);
- }
+ return null;
}
catch (Exception ex)
{
- Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex);
+ Debug.LogMessage(
+ ex,
+ "Error getting root tieLine: {Exception}",
+ this,
+ ex
+ );
return null;
}
-
- return null;
}
}
}
diff --git a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
index ffbe55092..c498fedf8 100644
--- a/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
+++ b/src/PepperDash.Essentials.Core/Web/EssentialsWebApi.cs
@@ -10,71 +10,71 @@
namespace PepperDash.Essentials.Core.Web
{
- ///
- /// Represents a EssentialsWebApi
- ///
- public class EssentialsWebApi : EssentialsDevice
- {
- private readonly WebApiServer _server;
+ ///
+ /// Represents a EssentialsWebApi
+ ///
+ public class EssentialsWebApi : EssentialsDevice
+ {
+ private readonly WebApiServer _server;
- ///
- /// http(s)://{ipaddress}/cws/{basePath}
- /// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
- ///
- private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
- ? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
- : "/api";
+ ///
+ /// http(s)://{ipaddress}/cws/{basePath}
+ /// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
+ ///
+ private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
+ ? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
+ : "/api";
- private const int DebugTrace = 0;
- private const int DebugInfo = 1;
- private const int DebugVerbose = 2;
+ private const int DebugTrace = 0;
+ private const int DebugInfo = 1;
+ private const int DebugVerbose = 2;
- ///
- /// Gets or sets the BasePath
- ///
- public string BasePath { get; private set; }
+ ///
+ /// Gets or sets the BasePath
+ ///
+ public string BasePath { get; private set; }
- ///
- /// Tracks if CWS is registered
- ///
- public bool IsRegistered
- {
- get { return _server.IsRegistered; }
- }
+ ///
+ /// Tracks if CWS is registered
+ ///
+ public bool IsRegistered
+ {
+ get { return _server.IsRegistered; }
+ }
- ///
- /// Constructor
- ///
- ///
- ///
- public EssentialsWebApi(string key, string name)
- : this(key, name, null)
- {
- }
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ public EssentialsWebApi(string key, string name)
+ : this(key, name, null)
+ {
+ }
- ///
- /// Constructor
- ///
- ///
- ///
- ///
- public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config)
- : base(key, name)
- {
- Key = key;
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ ///
+ public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config)
+ : base(key, name)
+ {
+ Key = key;
- if (config == null)
- BasePath = _defaultBasePath;
- else
- BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath;
+ if (config == null)
+ BasePath = _defaultBasePath;
+ else
+ BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath;
- _server = new WebApiServer(Key, Name, BasePath);
+ _server = new WebApiServer(Key, Name, BasePath);
- SetupRoutes();
- }
+ SetupRoutes();
+ }
- private void SetupRoutes()
- {
+ private void SetupRoutes()
+ {
var routes = new List
{
new HttpCwsRoute("versions")
@@ -177,6 +177,11 @@ private void SetupRoutes()
Name = "Get Routing Ports for a device",
RouteHandler = new GetRoutingPortsHandler()
},
+ new HttpCwsRoute("routingDevicesAndTieLines")
+ {
+ Name = "Get Routing Devices and TieLines",
+ RouteHandler = new GetRoutingDevicesAndTieLinesHandler()
+ },
};
AddRoute(routes);
@@ -211,78 +216,79 @@ public void AddRoute(List routes)
///
///
public override void Initialize()
- {
- AddRoute(new HttpCwsRoute("apiPaths") {
+ {
+ AddRoute(new HttpCwsRoute("apiPaths")
+ {
Name = "GetPaths",
RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath)
});
// If running on an appliance
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance)
- {
- /*
+ {
+ /*
WEBSERVER [ON | OFF | TIMEOUT | MAXSESSIONSPERUSER ]
*/
- var response = string.Empty;
- CrestronConsole.SendControlSystemCommand("webserver", ref response);
- if (response.Contains("OFF")) return;
+ var response = string.Empty;
+ CrestronConsole.SendControlSystemCommand("webserver", ref response);
+ if (response.Contains("OFF")) return;
- var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
- Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
+ var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
+ Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
- _server.Start();
+ _server.Start();
- GetPaths();
+ GetPaths();
- return;
- }
+ return;
+ }
- // Automatically start CWS when running on a server (Linux OS, Virtual Control)
- Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
+ // Automatically start CWS when running on a server (Linux OS, Virtual Control)
+ Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
- _server.Start();
+ _server.Start();
- GetPaths();
- }
+ GetPaths();
+ }
- ///
- /// Print the available pahts
- ///
- ///
- /// http(s)://{ipaddress}/cws/{basePath}
- /// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
- ///
- ///
- /// GetPaths method
- ///
- public void GetPaths()
- {
- Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
+ ///
+ /// Print the available pahts
+ ///
+ ///
+ /// http(s)://{ipaddress}/cws/{basePath}
+ /// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
+ ///
+ ///
+ /// GetPaths method
+ ///
+ public void GetPaths()
+ {
+ Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
+
+ var currentIp = CrestronEthernetHelper.GetEthernetParameter(
+ CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
- var currentIp = CrestronEthernetHelper.GetEthernetParameter(
- CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
-
- var hostname = CrestronEthernetHelper.GetEthernetParameter(
+ var hostname = CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}"
: $"https://{currentIp}/cws{BasePath}";
-
- Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
- var routeCollection = _server.GetRouteCollection();
- if (routeCollection == null)
- {
- Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null");
- return;
- }
- Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:");
- foreach (var route in routeCollection)
- {
- Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
- }
- Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
- }
- }
+ Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
+
+ var routeCollection = _server.GetRouteCollection();
+ if (routeCollection == null)
+ {
+ Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null");
+ return;
+ }
+ Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:");
+ foreach (var route in routeCollection)
+ {
+ Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
+ }
+ Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
+ }
+ }
}
\ No newline at end of file
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 000000000..0d65b0e35
--- /dev/null
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs
@@ -0,0 +1,178 @@
+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
+ {
+ public GetRoutingDevicesAndTieLinesHandler() : base(true) { }
+
+ 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
+ {
+ [JsonProperty("devices")]
+ public List Devices { get; set; }
+
+ [JsonProperty("tieLines")]
+ public List TieLines { get; set; }
+ }
+
+ ///
+ /// Represents a routing device with its ports information
+ ///
+ public class RoutingDeviceInfo
+ {
+ [JsonProperty("key")]
+ public string Key { get; set; }
+
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ [JsonProperty("hasInputs")]
+ public bool HasInputs { get; set; }
+
+ [JsonProperty("hasOutputs")]
+ public bool HasOutputs { get; set; }
+
+ [JsonProperty("hasInputsAndOutputs")]
+ public bool HasInputsAndOutputs { get; set; }
+
+ [JsonProperty("inputPorts", NullValueHandling = NullValueHandling.Ignore)]
+ public List InputPorts { get; set; }
+
+ [JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)]
+ public List OutputPorts { get; set; }
+ }
+
+ ///
+ /// Represents a routing port with its properties
+ ///
+ public class PortInfo
+ {
+ [JsonProperty("key")]
+ public string Key { get; set; }
+
+ [JsonProperty("signalType")]
+ public string SignalType { get; set; }
+
+ [JsonProperty("connectionType")]
+ public string ConnectionType { get; set; }
+
+ [JsonProperty("isInternal")]
+ public bool IsInternal { get; set; }
+ }
+
+ ///
+ /// Represents a tieline connection between two ports
+ ///
+ public class TieLineInfo
+ {
+ [JsonProperty("sourceDeviceKey")]
+ public string SourceDeviceKey { get; set; }
+
+ [JsonProperty("sourcePortKey")]
+ public string SourcePortKey { get; set; }
+
+ [JsonProperty("destinationDeviceKey")]
+ public string DestinationDeviceKey { get; set; }
+
+ [JsonProperty("destinationPortKey")]
+ public string DestinationPortKey { get; set; }
+
+ [JsonProperty("signalType")]
+ public string SignalType { get; set; }
+
+ [JsonProperty("isInternal")]
+ public bool IsInternal { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs
index ef19c955a..5fb7771ec 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 @@ private void StartSystem(object preventInitialization)
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 @@ public void LoadTieLines()
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)";
+ }
}
///