mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 19:08:29 +00:00
feat: Refactor routing signal types and enhance web API for routing devices
- Renamed `UsbOutput` to `Usb` in `eRoutingSignalType.cs`. - Removed unused `UsbInput` and `SecondaryAudio` signal types. - Added new HTTP route for retrieving routing devices and tielines in `EssentialsWebApi.cs`. - Implemented `GetRoutingDevicesAndTieLinesHandler` to handle requests for routing devices and tielines, including detailed port information. - Updated `GenericSink` to remove `SecondaryAudio` from input port signal types. - Created `ICurrentSourcesMessenger` to manage current source information and status updates. - Introduced `IDeviceInfoProviderMessenger` for device information communication with debounce functionality. - Updated `MobileControlSystemController` to use new messenger classes for current sources and device info. - Added console commands for listing tielines and visualizing routes in `ControlSystem.cs`. - Implemented methods for visualizing routes and current routes, including filtering options.
This commit is contained in:
parent
ec3e2bbc0b
commit
3d8ee22f45
14 changed files with 1433 additions and 456 deletions
|
|
@ -17,17 +17,129 @@ namespace PepperDash.Essentials.Core;
|
|||
/// </summary>
|
||||
public static class Extensions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Stores pending route requests, keyed by the destination device key.
|
||||
/// Used primarily to handle routing requests while a device is cooling down.
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, RouteRequest> RouteRequests = new Dictionary<string, RouteRequest>();
|
||||
|
||||
|
||||
/// <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.AudioVideo, new RouteDescriptorCollection() }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A queue to process route requests and releases sequentially.
|
||||
/// </summary>
|
||||
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>
|
||||
/// 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);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.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
|
|||
/// </summary>
|
||||
/// <param name="destination"></param>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
|
||||
/// <param name="destinationPort">The RoutingInputPort whose link is being checked for a route</param>
|
||||
/// <param name="sourcePort">The RoutingOutputPort whose link is being checked for a route</param>
|
||||
/// <param name="outputPortToUse">The RoutingOutputPort to use for the 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="signalType">This recursive function should not be called with AudioVideo</param>
|
||||
/// <param name="cycle">Just an informational counter</param>
|
||||
|
|
@ -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<TieLine> 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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}";
|
||||
}
|
||||
}
|
||||
|
||||
/*/// <summary>
|
||||
/// Represents an collection of individual route steps between Source and Destination
|
||||
/// </summary>
|
||||
public class RouteDescriptor<TInputSelector, TOutputSelector>
|
||||
{
|
||||
public IRoutingInputs<TInputSelector> Destination { get; private set; }
|
||||
public IRoutingOutputs<TOutputSelector> Source { get; private set; }
|
||||
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>
|
||||
/// Executes all routes described in this collection. Typically called via
|
||||
/// extension method IRoutingInputs.ReleaseAndMakeRoute()
|
||||
/// </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>
|
||||
/// Releases all routes in this collection. Typically called via
|
||||
/// extension method IRoutingInputs.ReleaseAndMakeRoute()
|
||||
/// </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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
|
@ -11,6 +11,9 @@ namespace PepperDash.Essentials.Core;
|
|||
/// </summary>
|
||||
public class RouteDescriptorCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static RouteDescriptorCollection DefaultCollection
|
||||
{
|
||||
get
|
||||
|
|
@ -24,6 +27,12 @@ public class RouteDescriptorCollection
|
|||
|
||||
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>
|
||||
/// 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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination device</param>
|
||||
/// <param name="inputPortKey">The input port key (optional)</param>
|
||||
/// <returns>The matching RouteDescriptor or null if not found</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/*/// <summary>
|
||||
/// A collection of RouteDescriptors - typically the static DefaultCollection is used
|
||||
/// </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;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
|
@ -1,54 +1,54 @@
|
|||
namespace PepperDash.Essentials.Core;
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a RouteSwitchDescriptor
|
||||
/// </summary>
|
||||
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>
|
||||
/// Represents a single switching step within a larger route, detailing the switching device, input port, and optionally the output port.
|
||||
/// </summary>
|
||||
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 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>
|
||||
/// 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")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class RoutingFeedbackManager : EssentialsDevice
|
||||
namespace PepperDash.Essentials.Core.Routing
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> 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.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key for this manager device.</param>
|
||||
/// <param name="name">The name of this manager device.</param>
|
||||
public RoutingFeedbackManager(string key, string name) : base(key, name)
|
||||
public class RoutingFeedbackManager : EssentialsDevice
|
||||
{
|
||||
AddPreActivationAction(SubscribeForMidpointFeedback);
|
||||
AddPreActivationAction(SubscribeForSinkFeedback);
|
||||
}
|
||||
/// <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, Timer> updateTimers = new Dictionary<string, Timer>();
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
|
||||
/// </summary>
|
||||
private void SubscribeForMidpointFeedback()
|
||||
{
|
||||
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
|
||||
/// <summary>
|
||||
/// Debounce delay in milliseconds
|
||||
/// </summary>
|
||||
private const long DEBOUNCE_MS = 500;
|
||||
|
||||
foreach (var device in midpointDevices)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The unique key for this manager device.</param>
|
||||
/// <param name="name">The name of this manager device.</param>
|
||||
public RoutingFeedbackManager(string key, string name)
|
||||
: base(key, name)
|
||||
{
|
||||
device.RouteChanged += HandleMidpointUpdate;
|
||||
AddPreActivationAction(BuildMidpointSinkMap);
|
||||
AddPreActivationAction(SubscribeForMidpointFeedback);
|
||||
AddPreActivationAction(SubscribeForSinkFeedback);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the InputChanged event on all devices implementing <see cref="IRoutingSinkWithSwitchingWithInputPort"/>.
|
||||
/// </summary>
|
||||
private void SubscribeForSinkFeedback()
|
||||
{
|
||||
var sinkDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||
|
||||
foreach (var device in sinkDevices)
|
||||
/// <summary>
|
||||
/// Builds a map of which sink devices are downstream of each midpoint device
|
||||
/// for performance optimization in HandleMidpointUpdate
|
||||
/// </summary>
|
||||
private void BuildMidpointSinkMap()
|
||||
{
|
||||
device.InputChanged += HandleSinkUpdate;
|
||||
}
|
||||
}
|
||||
midpointToSinksMap = new Dictionary<string, HashSet<string>>();
|
||||
|
||||
/// <summary>
|
||||
/// Handles the RouteChanged event from a midpoint device.
|
||||
/// Triggers an update for all sink devices.
|
||||
/// </summary>
|
||||
/// <param name="midpoint">The midpoint device that reported a route change.</param>
|
||||
/// <param name="newRoute">The descriptor of the new route.</param>
|
||||
private void HandleMidpointUpdate(IRoutingWithFeedback midpoint, RouteSwitchDescriptor newRoute)
|
||||
{
|
||||
try
|
||||
{
|
||||
var devices = DeviceManager.AllDevices.OfType<IRoutingSinkWithInputPort>();
|
||||
var sinks = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||
var midpoints = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
|
||||
|
||||
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<string>();
|
||||
|
||||
midpointToSinksMap[midpointKey].Add(sink.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(ex, "Error handling midpoint update from {midpointKey}:{Exception}", this, midpoint.Key, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the InputChanged event from a sink device.
|
||||
/// Triggers an update for the specific sink device.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sink device that reported an input change.</param>
|
||||
/// <param name="currentInputPort">The new input port selected on the sink device.</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination sink device to update.</param>
|
||||
/// <param name="inputPort">The currently selected input port on the destination device.</param>
|
||||
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
|
||||
/// <summary>
|
||||
/// Gets all upstream midpoint device keys for a given sink
|
||||
/// </summary>
|
||||
private HashSet<string> GetUpstreamMidpoints(IRoutingSinkWithSwitchingWithInputPort sink)
|
||||
{
|
||||
var tieLines = TieLineCollection.Default;
|
||||
var result = new HashSet<string>();
|
||||
var visited = new HashSet<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively 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.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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)
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
|
||||
/// </summary>
|
||||
private void SubscribeForMidpointFeedback()
|
||||
{
|
||||
Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex);
|
||||
return null;
|
||||
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
|
||||
|
||||
foreach (var device in midpointDevices)
|
||||
{
|
||||
device.RouteChanged += HandleMidpointUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
/// <summary>
|
||||
/// Subscribes to the InputChanged event on all devices implementing <see cref="IRoutingSinkWithSwitchingWithInputPort"/>.
|
||||
/// </summary>
|
||||
private void SubscribeForSinkFeedback()
|
||||
{
|
||||
var sinkDevices =
|
||||
DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||
|
||||
foreach (var device in sinkDevices)
|
||||
{
|
||||
device.InputChanged += HandleSinkUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the RouteChanged event from a midpoint device.
|
||||
/// Only triggers updates for sink devices that are downstream of this midpoint.
|
||||
/// </summary>
|
||||
/// <param name="midpoint">The midpoint device that reported a route change.</param>
|
||||
/// <param name="newRoute">The descriptor of the new route.</param>
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles the InputChanged event from a sink device.
|
||||
/// Triggers an update for the specific sink device.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sink device that reported an input change.</param>
|
||||
/// <param name="currentInputPort">The new input port selected on the sink device.</param>
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="destination">The destination sink device to update.</param>
|
||||
/// <param name="inputPort">The currently selected input port on the destination device.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately updates the CurrentSourceInfo for a destination device.
|
||||
/// Called after debounce delay.
|
||||
/// </summary>
|
||||
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<IEssentialsRoom>()
|
||||
.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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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<IRoutingOutputs>()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,16 +27,7 @@ namespace PepperDash.Essentials.Core
|
|||
/// <summary>
|
||||
/// Control signal type
|
||||
/// </summary>
|
||||
UsbOutput = 8,
|
||||
Usb = 8,
|
||||
|
||||
/// <summary>
|
||||
/// Control signal type
|
||||
/// </summary>
|
||||
UsbInput = 16,
|
||||
|
||||
/// <summary>
|
||||
/// Secondary audio signal type
|
||||
/// </summary>
|
||||
SecondaryAudio = 32
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue