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