mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-03-12 00:42:20 +00:00
Compare commits
4 Commits
developmen
...
tieline-vi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c9a643b6a | ||
|
|
fb8216beed | ||
|
|
d05ebecd7d | ||
|
|
d0fe225bbc |
@@ -436,14 +436,14 @@ namespace PepperDash.Essentials.Core
|
|||||||
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine);
|
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine);
|
||||||
foreach (var routingInputPort in inputPorts)
|
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;
|
if (outputPorts == null) return;
|
||||||
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine);
|
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine);
|
||||||
foreach (var routingOutputPort in outputPorts)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,20 @@ namespace PepperDash.Essentials.Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A collection of RouteDescriptors for each signal type.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly Dictionary<eRoutingSignalType, RouteDescriptorCollection> RouteDescriptors = new Dictionary<eRoutingSignalType, RouteDescriptorCollection>()
|
||||||
|
{
|
||||||
|
{ eRoutingSignalType.Audio, new RouteDescriptorCollection() },
|
||||||
|
{ eRoutingSignalType.Video, new RouteDescriptorCollection() },
|
||||||
|
{ eRoutingSignalType.SecondaryAudio, new RouteDescriptorCollection() },
|
||||||
|
{ eRoutingSignalType.AudioVideo, new RouteDescriptorCollection() },
|
||||||
|
{ eRoutingSignalType.UsbInput, new RouteDescriptorCollection() },
|
||||||
|
{ eRoutingSignalType.UsbOutput, new RouteDescriptorCollection() }
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores pending route requests, keyed by the destination device key.
|
/// Stores pending route requests, keyed by the destination device key.
|
||||||
/// Used primarily to handle routing requests while a device is cooling down.
|
/// Used primarily to handle routing requests while a device is cooling down.
|
||||||
@@ -29,6 +43,105 @@ namespace PepperDash.Essentials.Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly GenericQueue routeRequestQueue = new GenericQueue("routingQueue");
|
private static readonly GenericQueue routeRequestQueue = new GenericQueue("routingQueue");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indexed lookup of TieLines by destination device key for faster queries.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, List<TieLine>> _tieLinesByDestination;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indexed lookup of TieLines by source device key for faster queries.
|
||||||
|
/// </summary>
|
||||||
|
private static Dictionary<string, List<TieLine>> _tieLinesBySource;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Cache of failed route attempts to avoid re-checking impossible paths.
|
||||||
|
/// Format: "sourceKey|destKey|signalType"
|
||||||
|
/// </summary>
|
||||||
|
private static readonly HashSet<string> _impossibleRoutes = new HashSet<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indexes all TieLines by source and destination device keys for faster lookups.
|
||||||
|
/// Should be called once at system startup after all TieLines are created.
|
||||||
|
/// </summary>
|
||||||
|
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: ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets TieLines connected to a destination device.
|
||||||
|
/// Uses indexed lookup if available, otherwise falls back to LINQ query.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="destinationKey">The destination device key</param>
|
||||||
|
/// <returns>List of TieLines connected to the destination</returns>
|
||||||
|
private static IEnumerable<TieLine> GetTieLinesForDestination(string destinationKey)
|
||||||
|
{
|
||||||
|
if (_tieLinesByDestination != null && _tieLinesByDestination.TryGetValue(destinationKey, out List<TieLine> tieLines))
|
||||||
|
{
|
||||||
|
return tieLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to LINQ if index not available
|
||||||
|
return TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destinationKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets TieLines connected to a source device.
|
||||||
|
/// Uses indexed lookup if available, otherwise falls back to LINQ query.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceKey">The source device key</param>
|
||||||
|
/// <returns>List of TieLines connected to the source</returns>
|
||||||
|
private static IEnumerable<TieLine> GetTieLinesForSource(string sourceKey)
|
||||||
|
{
|
||||||
|
if (_tieLinesBySource != null && _tieLinesBySource.TryGetValue(sourceKey, out List<TieLine> tieLines))
|
||||||
|
{
|
||||||
|
return tieLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to LINQ if index not available
|
||||||
|
return TieLineCollection.Default.Where(t => t.SourcePort.ParentDevice.Key == sourceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a cache key for route impossibility tracking.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sourceKey">Source device key</param>
|
||||||
|
/// <param name="destKey">Destination device key</param>
|
||||||
|
/// <param name="type">Signal type</param>
|
||||||
|
/// <returns>Cache key string</returns>
|
||||||
|
private static string GetRouteKey(string sourceKey, string destKey, eRoutingSignalType type)
|
||||||
|
{
|
||||||
|
return string.Format("{0}|{1}|{2}", sourceKey, destKey, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime.
|
||||||
|
/// </summary>
|
||||||
|
public static void ClearImpossibleRoutesCache()
|
||||||
|
{
|
||||||
|
_impossibleRoutes.Clear();
|
||||||
|
Debug.LogMessage(LogEventLevel.Information, "Impossible routes cache cleared");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute
|
/// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute
|
||||||
/// and then attempts a new Route and if sucessful, stores that RouteDescriptor
|
/// and then attempts a new Route and if sucessful, stores that RouteDescriptor
|
||||||
@@ -173,8 +286,9 @@ namespace PepperDash.Essentials.Core
|
|||||||
if (!audioSuccess && !videoSuccess)
|
if (!audioSuccess && !videoSuccess)
|
||||||
return (null, null);
|
return (null, null);
|
||||||
|
|
||||||
|
// Return null for descriptors that have no routes
|
||||||
return (audioRouteDescriptor, videoRouteDescriptor);
|
return (audioSuccess && audioRouteDescriptor.Routes.Count > 0 ? audioRouteDescriptor : null,
|
||||||
|
videoSuccess && videoRouteDescriptor.Routes.Count > 0 ? videoRouteDescriptor : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -245,6 +359,90 @@ namespace PepperDash.Essentials.Core
|
|||||||
routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest));
|
routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps destination input ports to source output ports for all routing devices.
|
||||||
|
/// </summary>
|
||||||
|
public static void MapDestinationsToSources()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Index TieLines before mapping if not already done
|
||||||
|
if (_tieLinesByDestination == null || _tieLinesBySource == null)
|
||||||
|
{
|
||||||
|
IndexTieLines();
|
||||||
|
}
|
||||||
|
|
||||||
|
var sinks = DeviceManager.AllDevices.OfType<IRoutingInputs>().Where(d => !(d is IRoutingInputsOutputs));
|
||||||
|
var sources = DeviceManager.AllDevices.OfType<IRoutingOutputs>().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: ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the actual routing based on a <see cref="RouteRequest"/>.
|
/// Executes the actual routing based on a <see cref="RouteRequest"/>.
|
||||||
/// Finds the route path, adds it to the collection, and executes the switches.
|
/// Finds the route path, adds it to the collection, and executes the switches.
|
||||||
@@ -257,7 +455,51 @@ namespace PepperDash.Essentials.Core
|
|||||||
if (request.Source == null)
|
if (request.Source == null)
|
||||||
return;
|
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)
|
if (audioOrSingleRoute == null && videoRoute == null)
|
||||||
return;
|
return;
|
||||||
@@ -321,11 +563,13 @@ namespace PepperDash.Essentials.Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="destination"></param>
|
/// <param name="destination"></param>
|
||||||
/// <param name="source"></param>
|
/// <param name="source"></param>
|
||||||
/// <param name="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
|
/// <param name="outputPortToUse">The RoutingOutputPort whose link is being checked for a route</param>
|
||||||
/// <param name="alreadyCheckedDevices">Prevents Devices from being twice-checked</param>
|
/// <param name="alreadyCheckedDevices">Prevents Devices from being twice-checked</param>
|
||||||
/// <param name="signalType">This recursive function should not be called with AudioVideo</param>
|
/// <param name="signalType">This recursive function should not be called with AudioVideo</param>
|
||||||
/// <param name="cycle">Just an informational counter</param>
|
/// <param name="cycle">Just an informational counter</param>
|
||||||
/// <param name="routeTable">The RouteDescriptor being populated as the route is discovered</param>
|
/// <param name="routeTable">The RouteDescriptor being populated as the route is discovered</param>
|
||||||
|
/// <param name="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
|
||||||
|
/// <param name="sourcePort">The source output port (optional)</param>
|
||||||
/// <returns>true if source is hit</returns>
|
/// <returns>true if source is hit</returns>
|
||||||
private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
|
private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
|
||||||
RoutingOutputPort outputPortToUse, List<IRoutingInputsOutputs> alreadyCheckedDevices,
|
RoutingOutputPort outputPortToUse, List<IRoutingInputsOutputs> alreadyCheckedDevices,
|
||||||
@@ -333,42 +577,54 @@ namespace PepperDash.Essentials.Core
|
|||||||
{
|
{
|
||||||
cycle++;
|
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());
|
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;
|
RoutingInputPort goodInputPort = null;
|
||||||
|
|
||||||
|
// Use indexed lookup instead of LINQ query
|
||||||
|
var allDestinationTieLines = GetTieLinesForDestination(destination.Key);
|
||||||
|
|
||||||
IEnumerable<TieLine> destinationTieLines;
|
IEnumerable<TieLine> destinationTieLines;
|
||||||
TieLine directTie = null;
|
TieLine directTie = null;
|
||||||
|
|
||||||
if (destinationPort == null)
|
if (destinationPort == null)
|
||||||
{
|
{
|
||||||
destinationTieLines = TieLineCollection.Default.Where(t =>
|
destinationTieLines = allDestinationTieLines.Where(t =>
|
||||||
t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo));
|
t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo);
|
||||||
}
|
}
|
||||||
else
|
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
|
// find the TieLine without a port
|
||||||
if (destinationPort == null && sourcePort == null)
|
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
|
// find a tieLine to a specific destination port without a specific source port
|
||||||
else if (destinationPort != null && sourcePort == null)
|
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
|
// find a tieline to a specific source port without a specific destination port
|
||||||
else if (destinationPort == null & sourcePort != null)
|
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
|
// find a tieline to a specific source port and destination port
|
||||||
else if (destinationPort != null && sourcePort != null)
|
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
|
if (directTie != null) // Found a tie directly to the source
|
||||||
@@ -423,6 +679,10 @@ namespace PepperDash.Essentials.Core
|
|||||||
if (goodInputPort == null)
|
if (goodInputPort == null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key);
|
Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key);
|
||||||
|
|
||||||
|
// Cache this as an impossible route
|
||||||
|
_impossibleRoutes.Add(routeKey);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ namespace PepperDash.Essentials.Core
|
|||||||
{
|
{
|
||||||
if (route.SwitchingDevice is IRouting switchingDevice)
|
if (route.SwitchingDevice is IRouting switchingDevice)
|
||||||
{
|
{
|
||||||
if(clearRoute)
|
if (clearRoute)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -137,98 +137,11 @@ namespace PepperDash.Essentials.Core
|
|||||||
/// Returns a string representation of the route descriptor, including source, destination, and individual route steps.
|
/// Returns a string representation of the route descriptor, including source, destination, and individual route steps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>A string describing the route.</returns>
|
/// <returns>A string describing the route.</returns>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
var routesText = Routes.Select(r => r.ToString()).ToArray();
|
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)}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*/// <summary>
|
|
||||||
/// Represents an collection of individual route steps between Source and Destination
|
|
||||||
/// </summary>
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a RouteDescriptor
|
|
||||||
/// </summary>
|
|
||||||
public class RouteDescriptor<TInputSelector, TOutputSelector>
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the Destination
|
|
||||||
/// </summary>
|
|
||||||
public IRoutingInputs<TInputSelector> Destination { get; private set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the Source
|
|
||||||
/// </summary>
|
|
||||||
public IRoutingOutputs<TOutputSelector> Source { get; private set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the SignalType
|
|
||||||
/// </summary>
|
|
||||||
public eRoutingSignalType SignalType { get; private set; }
|
|
||||||
public List<RouteSwitchDescriptor<TInputSelector, TOutputSelector>> Routes { get; private set; }
|
|
||||||
|
|
||||||
|
|
||||||
public RouteDescriptor(IRoutingOutputs<TOutputSelector> source, IRoutingInputs<TInputSelector> destination, eRoutingSignalType signalType)
|
|
||||||
{
|
|
||||||
Destination = destination;
|
|
||||||
Source = source;
|
|
||||||
SignalType = signalType;
|
|
||||||
Routes = new List<RouteSwitchDescriptor<TInputSelector, TOutputSelector>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ExecuteRoutes method
|
|
||||||
/// </summary>
|
|
||||||
public void ExecuteRoutes()
|
|
||||||
{
|
|
||||||
foreach (var route in Routes)
|
|
||||||
{
|
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString());
|
|
||||||
|
|
||||||
if (route.SwitchingDevice is IRoutingSinkWithSwitching<TInputSelector> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ReleaseRoutes method
|
|
||||||
/// </summary>
|
|
||||||
public void ReleaseRoutes()
|
|
||||||
{
|
|
||||||
foreach (var route in Routes)
|
|
||||||
{
|
|
||||||
if (route.SwitchingDevice is IRouting<TInputSelector, TOutputSelector>)
|
|
||||||
{
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ToString method
|
|
||||||
/// </summary>
|
|
||||||
/// <inheritdoc />
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using PepperDash.Core;
|
using System.Collections.Generic;
|
||||||
using Serilog.Events;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using PepperDash.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core
|
namespace PepperDash.Essentials.Core
|
||||||
@@ -11,6 +11,9 @@ namespace PepperDash.Essentials.Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RouteDescriptorCollection
|
public class RouteDescriptorCollection
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the default collection of RouteDescriptors.
|
||||||
|
/// </summary>
|
||||||
public static RouteDescriptorCollection DefaultCollection
|
public static RouteDescriptorCollection DefaultCollection
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -24,6 +27,11 @@ namespace PepperDash.Essentials.Core
|
|||||||
|
|
||||||
private readonly List<RouteDescriptor> RouteDescriptors = new List<RouteDescriptor>();
|
private readonly List<RouteDescriptor> RouteDescriptors = new List<RouteDescriptor>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets an enumerable collection of all RouteDescriptors in this collection.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<RouteDescriptor> Descriptors => RouteDescriptors.AsReadOnly();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the
|
/// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the
|
||||||
/// destination exists already, it will not be added - in order to preserve
|
/// destination exists already, it will not be added - in order to preserve
|
||||||
@@ -37,13 +45,29 @@ namespace PepperDash.Essentials.Core
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination)
|
// Check if a route already exists with the same source, destination, input port, AND signal type
|
||||||
&& RouteDescriptors.Any(t => t.Destination == descriptor.Destination && t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key))
|
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,
|
Debug.LogMessage(LogEventLevel.Information, descriptor.Destination,
|
||||||
"Route to [{0}] already exists in global routes table", descriptor?.Source?.Key);
|
"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;
|
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);
|
RouteDescriptors.Add(descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +81,12 @@ namespace PepperDash.Essentials.Core
|
|||||||
return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination);
|
return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the route descriptor for a specific destination and input port
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="destination">The destination device</param>
|
||||||
|
/// <param name="inputPortKey">The input port key</param>
|
||||||
|
/// <returns>The matching RouteDescriptor or null if not found</returns>
|
||||||
public RouteDescriptor GetRouteDescriptorForDestinationAndInputPort(IRoutingInputs destination, string inputPortKey)
|
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);
|
Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
|
||||||
@@ -84,70 +114,4 @@ namespace PepperDash.Essentials.Core
|
|||||||
return descr;
|
return descr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*/// <summary>
|
|
||||||
/// A collection of RouteDescriptors - typically the static DefaultCollection is used
|
|
||||||
/// </summary>
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a RouteDescriptorCollection
|
|
||||||
/// </summary>
|
|
||||||
public class RouteDescriptorCollection<TInputSelector, TOutputSelector>
|
|
||||||
{
|
|
||||||
public static RouteDescriptorCollection<TInputSelector, TOutputSelector> DefaultCollection
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_DefaultCollection == null)
|
|
||||||
_DefaultCollection = new RouteDescriptorCollection<TInputSelector, TOutputSelector>();
|
|
||||||
return _DefaultCollection;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private static RouteDescriptorCollection<TInputSelector, TOutputSelector> _DefaultCollection;
|
|
||||||
|
|
||||||
private readonly List<RouteDescriptor> RouteDescriptors = new List<RouteDescriptor>();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="descriptor"></param>
|
|
||||||
/// <summary>
|
|
||||||
/// AddRouteDescriptor method
|
|
||||||
/// </summary>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the RouteDescriptor for a destination
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>null if no RouteDescriptor for a destination exists</returns>
|
|
||||||
/// <summary>
|
|
||||||
/// GetRouteDescriptorForDestination method
|
|
||||||
/// </summary>
|
|
||||||
public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs<TInputSelector> destination)
|
|
||||||
{
|
|
||||||
return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the RouteDescriptor for a given destination AND removes it from collection.
|
|
||||||
/// Returns null if no route with the provided destination exists.
|
|
||||||
/// </summary>
|
|
||||||
public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs<TInputSelector> destination)
|
|
||||||
{
|
|
||||||
var descr = GetRouteDescriptorForDestination(destination);
|
|
||||||
if (descr != null)
|
|
||||||
RouteDescriptors.Remove(descr);
|
|
||||||
return descr;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
@@ -4,96 +4,51 @@
|
|||||||
/// Represents a RouteSwitchDescriptor
|
/// Represents a RouteSwitchDescriptor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RouteSwitchDescriptor
|
public class RouteSwitchDescriptor
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the SwitchingDevice
|
|
||||||
/// </summary>
|
|
||||||
public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } }
|
|
||||||
/// <summary>
|
|
||||||
/// The output port being switched from (relevant for matrix switchers). Null for sink devices.
|
|
||||||
/// </summary>
|
|
||||||
public RoutingOutputPort OutputPort { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The input port being switched to.
|
|
||||||
/// </summary>
|
|
||||||
public RoutingInputPort InputPort { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port).
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="inputPort">The input port being switched to.</param>
|
|
||||||
public RouteSwitchDescriptor(RoutingInputPort inputPort)
|
|
||||||
{
|
|
||||||
InputPort = inputPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for matrix switchers.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="outputPort">The output port being switched from.</param>
|
|
||||||
/// <param name="inputPort">The input port being switched to.</param>
|
|
||||||
public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort)
|
|
||||||
{
|
|
||||||
InputPort = inputPort;
|
|
||||||
OutputPort = outputPort;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns a string representation of the route switch descriptor.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A string describing the switch operation.</returns>
|
|
||||||
/// <inheritdoc />
|
|
||||||
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")}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*/// <summary>
|
|
||||||
/// Represents an individual link for a route
|
|
||||||
/// </summary>
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a RouteSwitchDescriptor
|
|
||||||
/// </summary>
|
|
||||||
public class RouteSwitchDescriptor<TInputSelector, TOutputSelector>
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the SwitchingDevice
|
/// Gets or sets the SwitchingDevice
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IRoutingInputs<TInputSelector> SwitchingDevice { get { return InputPort.ParentDevice; } }
|
public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the OutputPort
|
/// The output port being switched from (relevant for matrix switchers). Null for sink devices.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RoutingOutputPort<TOutputSelector> OutputPort { get; set; }
|
public RoutingOutputPort OutputPort { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the InputPort
|
/// The input port being switched to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RoutingInputPort<TInputSelector> InputPort { get; set; }
|
public RoutingInputPort InputPort { get; set; }
|
||||||
|
|
||||||
public RouteSwitchDescriptor(RoutingInputPort<TInputSelector> inputPort)
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="inputPort">The input port being switched to.</param>
|
||||||
|
public RouteSwitchDescriptor(RoutingInputPort inputPort)
|
||||||
{
|
{
|
||||||
InputPort = inputPort;
|
InputPort = inputPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RouteSwitchDescriptor(RoutingOutputPort<TOutputSelector> outputPort, RoutingInputPort<TInputSelector> inputPort)
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for matrix switchers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="outputPort">The output port being switched from.</param>
|
||||||
|
/// <param name="inputPort">The input port being switched to.</param>
|
||||||
|
public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort)
|
||||||
{
|
{
|
||||||
InputPort = inputPort;
|
InputPort = inputPort;
|
||||||
OutputPort = outputPort;
|
OutputPort = outputPort;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ToString method
|
/// Returns a string representation of the route switch descriptor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>A string describing the switch operation.</returns>
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
if (SwitchingDevice is IRouting)
|
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
|
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")}";
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Crestron.SimplSharp;
|
||||||
using PepperDash.Core;
|
using PepperDash.Core;
|
||||||
using PepperDash.Essentials.Core.Config;
|
using PepperDash.Essentials.Core.Config;
|
||||||
|
|
||||||
@@ -11,6 +13,21 @@ namespace PepperDash.Essentials.Core.Routing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RoutingFeedbackManager : EssentialsDevice
|
public class RoutingFeedbackManager : EssentialsDevice
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maps midpoint device keys to the set of sink device keys that are downstream
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<string, HashSet<string>> midpointToSinksMap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Debounce timers for each sink device to prevent rapid successive updates
|
||||||
|
/// </summary>
|
||||||
|
private readonly Dictionary<string, CTimer> updateTimers = new Dictionary<string, CTimer>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Debounce delay in milliseconds
|
||||||
|
/// </summary>
|
||||||
|
private const long DEBOUNCE_MS = 500;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
|
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -19,10 +36,100 @@ namespace PepperDash.Essentials.Core.Routing
|
|||||||
public RoutingFeedbackManager(string key, string name)
|
public RoutingFeedbackManager(string key, string name)
|
||||||
: base(key, name)
|
: base(key, name)
|
||||||
{
|
{
|
||||||
|
AddPreActivationAction(BuildMidpointSinkMap);
|
||||||
AddPreActivationAction(SubscribeForMidpointFeedback);
|
AddPreActivationAction(SubscribeForMidpointFeedback);
|
||||||
AddPreActivationAction(SubscribeForSinkFeedback);
|
AddPreActivationAction(SubscribeForSinkFeedback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a map of which sink devices are downstream of each midpoint device
|
||||||
|
/// for performance optimization in HandleMidpointUpdate
|
||||||
|
/// </summary>
|
||||||
|
private void BuildMidpointSinkMap()
|
||||||
|
{
|
||||||
|
midpointToSinksMap = new Dictionary<string, HashSet<string>>();
|
||||||
|
|
||||||
|
var sinks = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||||
|
var midpoints = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
midpointToSinksMap[midpointKey].Add(sink.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogMessage(
|
||||||
|
Serilog.Events.LogEventLevel.Information,
|
||||||
|
"Built midpoint-to-sink map with {count} midpoints",
|
||||||
|
this,
|
||||||
|
midpointToSinksMap.Count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all upstream midpoint device keys for a given sink
|
||||||
|
/// </summary>
|
||||||
|
private HashSet<string> GetUpstreamMidpoints(IRoutingSinkWithSwitchingWithInputPort sink)
|
||||||
|
{
|
||||||
|
var result = new HashSet<string>();
|
||||||
|
var visited = new HashSet<string>();
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Recursively traces upstream to find all midpoint devices
|
||||||
|
/// </summary>
|
||||||
|
private void TraceUpstreamMidpoints(TieLine tieLine, HashSet<string> midpoints, HashSet<string> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
|
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -52,7 +159,7 @@ namespace PepperDash.Essentials.Core.Routing
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the RouteChanged event from a midpoint device.
|
/// 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.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="midpoint">The midpoint device that reported a route change.</param>
|
/// <param name="midpoint">The midpoint device that reported a route change.</param>
|
||||||
/// <param name="newRoute">The descriptor of the new route.</param>
|
/// <param name="newRoute">The descriptor of the new route.</param>
|
||||||
@@ -63,12 +170,33 @@ namespace PepperDash.Essentials.Core.Routing
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var devices =
|
// Only update affected sinks (performance optimization)
|
||||||
DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
if (midpointToSinksMap != null && midpointToSinksMap.TryGetValue(midpoint.Key, out var affectedSinkKeys))
|
||||||
|
|
||||||
foreach (var device in devices)
|
|
||||||
{
|
{
|
||||||
UpdateDestination(device, device.CurrentInputPort);
|
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)
|
catch (Exception ex)
|
||||||
@@ -113,6 +241,7 @@ namespace PepperDash.Essentials.Core.Routing
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Updates the CurrentSourceInfo and CurrentSourceInfoKey properties on a destination (sink) device
|
/// 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.
|
/// based on its currently selected input port by tracing the route back through tie lines.
|
||||||
|
/// Uses debouncing to prevent rapid successive updates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="destination">The destination sink device to update.</param>
|
/// <param name="destination">The destination sink device to update.</param>
|
||||||
/// <param name="inputPort">The currently selected input port on the destination device.</param>
|
/// <param name="inputPort">The currently selected input port on the destination device.</param>
|
||||||
@@ -120,6 +249,55 @@ namespace PepperDash.Essentials.Core.Routing
|
|||||||
IRoutingSinkWithSwitching destination,
|
IRoutingSinkWithSwitching destination,
|
||||||
RoutingInputPort inputPort
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immediately updates the CurrentSourceInfo for a destination device.
|
||||||
|
/// Called after debounce delay.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateDestinationImmediate(
|
||||||
|
IRoutingSinkWithSwitching destination,
|
||||||
|
RoutingInputPort inputPort
|
||||||
|
)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(
|
Debug.LogMessage(
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
Serilog.Events.LogEventLevel.Debug,
|
||||||
@@ -309,105 +487,98 @@ namespace PepperDash.Essentials.Core.Routing
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Recursively traces a route back from a given tie line to find the root source tie line.
|
/// Traces a route back from a given tie line to find the root source tie line.
|
||||||
/// It navigates through midpoint devices (<see cref="IRoutingWithFeedback"/>) by checking their current routes.
|
/// Leverages the existing Extensions.GetRouteToSource method with loop protection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="tieLine">The starting tie line (typically connected to a sink or midpoint).</param>
|
/// <param name="tieLine">The starting tie line (typically connected to a sink or midpoint).</param>
|
||||||
/// <returns>The <see cref="TieLine"/> connected to the original source device, or null if the source cannot be determined.</returns>
|
/// <returns>The <see cref="TieLine"/> connected to the original source device, or null if the source cannot be determined.</returns>
|
||||||
private TieLine GetRootTieLine(TieLine tieLine)
|
private TieLine GetRootTieLine(TieLine tieLine)
|
||||||
{
|
{
|
||||||
TieLine nextTieLine = null;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine);
|
if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink))
|
||||||
|
|
||||||
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
|
|
||||||
{
|
{
|
||||||
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint);
|
Debug.LogMessage(
|
||||||
|
Serilog.Events.LogEventLevel.Debug,
|
||||||
if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0)
|
"TieLine destination {device} is not IRoutingInputs",
|
||||||
{
|
this,
|
||||||
Debug.LogMessage(
|
tieLine.DestinationPort.ParentDevice.Key
|
||||||
Serilog.Events.LogEventLevel.Debug,
|
);
|
||||||
"Midpoint {midpointKey} has no routes",
|
return null;
|
||||||
this,
|
|
||||||
midpoint.Key
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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.Debug,
|
|
||||||
"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);
|
// Get all potential sources (devices that only have outputs, not inputs+outputs)
|
||||||
//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));
|
var sources = DeviceManager.AllDevices
|
||||||
|
.OfType<IRoutingOutputs>()
|
||||||
|
.Where(s => !(s is IRoutingInputsOutputs));
|
||||||
|
|
||||||
if (
|
// Try each signal type that this TieLine supports
|
||||||
tieLine.SourcePort.ParentDevice is IRoutingSource
|
var signalTypes = new[]
|
||||||
|| tieLine.SourcePort.ParentDevice is IRoutingOutputs
|
|
||||||
) //end of the chain
|
|
||||||
{
|
{
|
||||||
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root: {tieLine}", this, tieLine);
|
eRoutingSignalType.Audio,
|
||||||
return tieLine;
|
eRoutingSignalType.Video,
|
||||||
|
eRoutingSignalType.AudioVideo,
|
||||||
|
eRoutingSignalType.SecondaryAudio,
|
||||||
|
eRoutingSignalType.UsbInput,
|
||||||
|
eRoutingSignalType.UsbOutput
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
Debug.LogMessage(
|
||||||
tl.DestinationPort.Key == tieLine.SourcePort.Key
|
Serilog.Events.LogEventLevel.Debug,
|
||||||
&& tl.DestinationPort.ParentDevice.Key == tieLine.SourcePort.ParentDevice.Key
|
"No route found to any source from {sink}",
|
||||||
|
this,
|
||||||
|
sink.Key
|
||||||
);
|
);
|
||||||
|
return null;
|
||||||
if (nextTieLine != null)
|
|
||||||
{
|
|
||||||
return GetRootTieLine(nextTieLine);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,71 +10,71 @@ using Serilog.Events;
|
|||||||
|
|
||||||
namespace PepperDash.Essentials.Core.Web
|
namespace PepperDash.Essentials.Core.Web
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a EssentialsWebApi
|
/// Represents a EssentialsWebApi
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EssentialsWebApi : EssentialsDevice
|
public class EssentialsWebApi : EssentialsDevice
|
||||||
{
|
{
|
||||||
private readonly WebApiServer _server;
|
private readonly WebApiServer _server;
|
||||||
|
|
||||||
///<example>
|
///<example>
|
||||||
/// http(s)://{ipaddress}/cws/{basePath}
|
/// http(s)://{ipaddress}/cws/{basePath}
|
||||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
||||||
/// </example>
|
/// </example>
|
||||||
private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
|
private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
|
||||||
? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
|
? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
|
||||||
: "/api";
|
: "/api";
|
||||||
|
|
||||||
private const int DebugTrace = 0;
|
private const int DebugTrace = 0;
|
||||||
private const int DebugInfo = 1;
|
private const int DebugInfo = 1;
|
||||||
private const int DebugVerbose = 2;
|
private const int DebugVerbose = 2;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the BasePath
|
/// Gets or sets the BasePath
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string BasePath { get; private set; }
|
public string BasePath { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tracks if CWS is registered
|
/// Tracks if CWS is registered
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsRegistered
|
public bool IsRegistered
|
||||||
{
|
{
|
||||||
get { return _server.IsRegistered; }
|
get { return _server.IsRegistered; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="name"></param>
|
/// <param name="name"></param>
|
||||||
public EssentialsWebApi(string key, string name)
|
public EssentialsWebApi(string key, string name)
|
||||||
: this(key, name, null)
|
: this(key, name, null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Constructor
|
/// Constructor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="name"></param>
|
/// <param name="name"></param>
|
||||||
/// <param name="config"></param>
|
/// <param name="config"></param>
|
||||||
public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config)
|
public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config)
|
||||||
: base(key, name)
|
: base(key, name)
|
||||||
{
|
{
|
||||||
Key = key;
|
Key = key;
|
||||||
|
|
||||||
if (config == null)
|
if (config == null)
|
||||||
BasePath = _defaultBasePath;
|
BasePath = _defaultBasePath;
|
||||||
else
|
else
|
||||||
BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath;
|
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<HttpCwsRoute>
|
var routes = new List<HttpCwsRoute>
|
||||||
{
|
{
|
||||||
new HttpCwsRoute("versions")
|
new HttpCwsRoute("versions")
|
||||||
@@ -177,6 +177,11 @@ namespace PepperDash.Essentials.Core.Web
|
|||||||
Name = "Get Routing Ports for a device",
|
Name = "Get Routing Ports for a device",
|
||||||
RouteHandler = new GetRoutingPortsHandler()
|
RouteHandler = new GetRoutingPortsHandler()
|
||||||
},
|
},
|
||||||
|
new HttpCwsRoute("routingDevicesAndTieLines")
|
||||||
|
{
|
||||||
|
Name = "Get Routing Devices and TieLines",
|
||||||
|
RouteHandler = new GetRoutingDevicesAndTieLinesHandler()
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
AddRoute(routes);
|
AddRoute(routes);
|
||||||
@@ -211,78 +216,79 @@ namespace PepperDash.Essentials.Core.Web
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
AddRoute(new HttpCwsRoute("apiPaths") {
|
AddRoute(new HttpCwsRoute("apiPaths")
|
||||||
|
{
|
||||||
Name = "GetPaths",
|
Name = "GetPaths",
|
||||||
RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath)
|
RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath)
|
||||||
});
|
});
|
||||||
|
|
||||||
// If running on an appliance
|
// If running on an appliance
|
||||||
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance)
|
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance)
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
WEBSERVER [ON | OFF | TIMEOUT <VALUE IN SECONDS> | MAXSESSIONSPERUSER <Number of sessions>]
|
WEBSERVER [ON | OFF | TIMEOUT <VALUE IN SECONDS> | MAXSESSIONSPERUSER <Number of sessions>]
|
||||||
*/
|
*/
|
||||||
var response = string.Empty;
|
var response = string.Empty;
|
||||||
CrestronConsole.SendControlSystemCommand("webserver", ref response);
|
CrestronConsole.SendControlSystemCommand("webserver", ref response);
|
||||||
if (response.Contains("OFF")) return;
|
if (response.Contains("OFF")) return;
|
||||||
|
|
||||||
var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
|
var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
|
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)
|
// Automatically start CWS when running on a server (Linux OS, Virtual Control)
|
||||||
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
|
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
|
||||||
|
|
||||||
_server.Start();
|
_server.Start();
|
||||||
|
|
||||||
GetPaths();
|
GetPaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Print the available pahts
|
/// Print the available pahts
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <example>
|
/// <example>
|
||||||
/// http(s)://{ipaddress}/cws/{basePath}
|
/// http(s)://{ipaddress}/cws/{basePath}
|
||||||
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
|
||||||
/// </example>
|
/// </example>
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// GetPaths method
|
/// GetPaths method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void GetPaths()
|
public void GetPaths()
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
||||||
|
|
||||||
var currentIp = CrestronEthernetHelper.GetEthernetParameter(
|
var currentIp = CrestronEthernetHelper.GetEthernetParameter(
|
||||||
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
|
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);
|
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
|
||||||
|
|
||||||
var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
|
var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
|
||||||
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}"
|
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}"
|
||||||
: $"https://{currentIp}/cws{BasePath}";
|
: $"https://{currentIp}/cws{BasePath}";
|
||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
|
Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
|
||||||
|
|
||||||
var routeCollection = _server.GetRouteCollection();
|
var routeCollection = _server.GetRouteCollection();
|
||||||
if (routeCollection == null)
|
if (routeCollection == null)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null");
|
Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:");
|
Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:");
|
||||||
foreach (var route in routeCollection)
|
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, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
|
||||||
}
|
}
|
||||||
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Handles HTTP requests to retrieve routing devices and tielines information
|
||||||
|
/// </summary>
|
||||||
|
public class GetRoutingDevicesAndTieLinesHandler : WebApiBaseRequestHandler
|
||||||
|
{
|
||||||
|
public GetRoutingDevicesAndTieLinesHandler() : base(true) { }
|
||||||
|
|
||||||
|
protected override void HandleGet(HttpCwsContext context)
|
||||||
|
{
|
||||||
|
var devices = new List<RoutingDeviceInfo>();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the complete routing system information including devices and tielines
|
||||||
|
/// </summary>
|
||||||
|
public class RoutingSystemInfo
|
||||||
|
{
|
||||||
|
[JsonProperty("devices")]
|
||||||
|
public List<RoutingDeviceInfo> Devices { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("tieLines")]
|
||||||
|
public List<TieLineInfo> TieLines { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a routing device with its ports information
|
||||||
|
/// </summary>
|
||||||
|
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<PortInfo> InputPorts { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
|
public List<PortInfo> OutputPorts { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a routing port with its properties
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a tieline connection between two ports
|
||||||
|
/// </summary>
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -92,12 +93,16 @@ namespace PepperDash.Essentials
|
|||||||
|
|
||||||
CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator);
|
CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator);
|
||||||
|
|
||||||
CrestronConsole.AddNewConsoleCommand(s =>
|
CrestronConsole.AddNewConsoleCommand(ListTieLines,
|
||||||
{
|
"listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator);
|
||||||
foreach (var tl in TieLineCollection.Default)
|
|
||||||
CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine);
|
CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes",
|
||||||
},
|
"Visualizes routes by signal type",
|
||||||
"listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator);
|
ConsoleAccessLevelEnum.AccessOperator);
|
||||||
|
|
||||||
|
CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes",
|
||||||
|
"Visualizes current active routes from DefaultCollection",
|
||||||
|
ConsoleAccessLevelEnum.AccessOperator);
|
||||||
|
|
||||||
CrestronConsole.AddNewConsoleCommand(s =>
|
CrestronConsole.AddNewConsoleCommand(s =>
|
||||||
{
|
{
|
||||||
@@ -443,6 +448,282 @@ namespace PepperDash.Essentials
|
|||||||
|
|
||||||
Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded.");
|
Debug.LogMessage(LogEventLevel.Information, "All Tie Lines Loaded.");
|
||||||
|
|
||||||
|
Extensions.MapDestinationsToSources();
|
||||||
|
|
||||||
|
Debug.LogMessage(LogEventLevel.Information, "All Routes Mapped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Visualizes routes in a tree format for better understanding of signal paths
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses route filter arguments from command line
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Command line arguments</param>
|
||||||
|
/// <param name="signalTypeFilter">Parsed signal type filter (if any)</param>
|
||||||
|
/// <param name="sourceFilter">Parsed source filter (if any)</param>
|
||||||
|
/// <param name="destFilter">Parsed destination filter (if any)</param>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Visualizes a single route descriptor in a tree format
|
||||||
|
/// </summary>
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a readable description of the switching operation
|
||||||
|
/// </summary>
|
||||||
|
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)";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user