From d0fe225bbc2455f8d7428d69a1979c98c34723fc Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 14 Jan 2026 09:54:44 -0600 Subject: [PATCH 1/4] feat: improve routing feedback manager * Performance improvment by mapping out midpoint to sinks on startup * Use existing routing methods * Debounce event handling * Check all signal types for route updates --- .../Routing/RoutingFeedbackManager.cs | 331 +++++++++++++----- 1 file changed, 251 insertions(+), 80 deletions(-) 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; } } } From d05ebecd7d09719f55016229cceb374a0657242f Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 14 Jan 2026 09:55:32 -0600 Subject: [PATCH 2/4] fix: getroutingports command now prints port types --- src/PepperDash.Essentials.Core/Devices/DeviceManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); } } From fb8216beedb94990f76495358d1a9c8917c9362a Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 14 Jan 2026 10:00:48 -0600 Subject: [PATCH 3/4] feat: map routes/tielines at startup and new console commands * visualizeroutes allows visualizing configured routes based on tielines and signal type * can be filtered by source key, destination key, and type, along with partial matches for source & destination keys * visualizecurrentroutes visualizes what Essentials says is currently routed by type * uses same filtering as visualizeroutes * improvements to how the routing algorithm works --- .../Routing/Extensions.cs | 282 ++++++++++++++++- .../Routing/RouteDescriptor.cs | 95 +----- .../Routing/RouteDescriptorCollection.cs | 112 +++---- .../Routing/RouteSwitchDescriptor.cs | 87 ++---- src/PepperDash.Essentials/ControlSystem.cs | 293 +++++++++++++++++- 5 files changed, 621 insertions(+), 248 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index 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/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)"; + } } /// From 9c9a643b6a57eda1e9eb40c1e765986fcda4c5d0 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 14 Jan 2026 10:22:35 -0600 Subject: [PATCH 4/4] feat: add CWS endpoint to get routing devices & tielines together --- .../Web/EssentialsWebApi.cs | 218 +++++++++--------- .../GetRoutingDevicesAndTieLinesHandler.cs | 178 ++++++++++++++ 2 files changed, 290 insertions(+), 106 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/Web/RequestHandlers/GetRoutingDevicesAndTieLinesHandler.cs 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; } + } +}