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:
Neil Dorin 2026-04-10 09:49:16 -06:00
parent ec3e2bbc0b
commit 3d8ee22f45
14 changed files with 1433 additions and 456 deletions

View file

@ -469,14 +469,14 @@ public static class DeviceManager
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);
} }
} }

View file

@ -17,17 +17,129 @@ namespace PepperDash.Essentials.Core;
/// </summary> /// </summary>
public static class Extensions public static class Extensions
{ {
/// <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.
/// </summary> /// </summary>
private static readonly Dictionary<string, RouteRequest> RouteRequests = new Dictionary<string, RouteRequest>(); 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> /// <summary>
/// A queue to process route requests and releases sequentially. /// A queue to process route requests and releases sequentially.
/// </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
@ -108,8 +220,7 @@ public static class Extensions
public static (RouteDescriptor, RouteDescriptor) GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source, eRoutingSignalType signalType, RoutingInputPort destinationPort, RoutingOutputPort sourcePort) 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 it's a single signal type, find the route
if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) && if (!signalType.HasFlag(eRoutingSignalType.AudioVideo))
!(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio)))
{ {
var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, signalType); 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); 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); 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)) var audioSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort);
{
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);
if (!audioSuccess) if (!audioSuccess)
Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key); Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key);
@ -165,10 +268,12 @@ public static class Extensions
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>
/// Internal method to handle the logic for releasing an existing route and making a new one. /// 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. /// Handles devices with cooling states by queueing the request.
@ -232,7 +337,7 @@ 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); Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
} }
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination,destinationPort?.Key ?? string.Empty, false)); routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false));
routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest)); routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest));
} }
@ -249,7 +354,48 @@ public static class Extensions
if (request.Source == null) 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
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) if (audioOrSingleRoute == null && videoRoute == null)
return; return;
@ -265,7 +411,8 @@ public static class Extensions
audioOrSingleRoute.ExecuteRoutes(); audioOrSingleRoute.ExecuteRoutes();
videoRoute?.ExecuteRoutes(); videoRoute?.ExecuteRoutes();
} catch(Exception ex) }
catch (Exception ex)
{ {
Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request); 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); Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key);
current.ReleaseRoutes(clearRoute); 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> /// </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="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="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="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>
@ -325,42 +549,53 @@ public static class Extensions
{ {
cycle++; 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()); 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
@ -415,6 +650,10 @@ public static class Extensions
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;
} }

View file

@ -101,7 +101,7 @@ public class RouteDescriptor
{ {
if (route.SwitchingDevice is IRouting switchingDevice) if (route.SwitchingDevice is IRouting switchingDevice)
{ {
if(clearRoute) if (clearRoute)
{ {
try try
{ {
@ -138,77 +138,6 @@ public class RouteDescriptor
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>
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));
}
}*/

View file

@ -11,6 +11,9 @@ namespace PepperDash.Essentials.Core;
/// </summary> /// </summary>
public class RouteDescriptorCollection 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 public static RouteDescriptorCollection DefaultCollection
{ {
get get
@ -24,6 +27,12 @@ public class RouteDescriptorCollection
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 +46,29 @@ public class RouteDescriptorCollection
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);
} }
@ -58,6 +83,12 @@ public class RouteDescriptorCollection
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);
@ -68,6 +99,9 @@ public class RouteDescriptorCollection
/// Returns the RouteDescriptor for a given destination AND removes it from collection. /// Returns the RouteDescriptor for a given destination AND removes it from collection.
/// Returns null if no route with the provided destination exists. /// Returns null if no route with the provided destination exists.
/// </summary> /// </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 = "") 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); Debug.LogMessage(LogEventLevel.Information, "Removing route descriptor for '{destination}':'{inputPortKey}'", destination.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
@ -83,66 +117,3 @@ public class RouteDescriptorCollection
return descr; 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;
}
}*/

View file

@ -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> /// <summary>
/// Represents a single switching step within a larger route, detailing the switching device, input port, and optionally the output port. /// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port).
/// </summary> /// </summary>
public class RouteSwitchDescriptor /// <param name="inputPort">The input port being switched to.</param>
{ public RouteSwitchDescriptor(RoutingInputPort inputPort)
/// <summary> {
/// Gets or sets the SwitchingDevice InputPort = inputPort;
/// </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> /// <summary>
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port). /// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for matrix switchers.
/// </summary> /// </summary>
/// <param name="inputPort">The input port being switched to.</param> /// <param name="outputPort">The output port being switched from.</param>
public RouteSwitchDescriptor(RoutingInputPort inputPort) /// <param name="inputPort">The input port being switched to.</param>
{ public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort)
InputPort = inputPort; {
} InputPort = inputPort;
OutputPort = outputPort;
/// <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>
/// 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")}";
}
}
}

View file

@ -1,248 +1,562 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Timers;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.Config;
namespace PepperDash.Essentials.Core.Routing; 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
{ {
/// <summary> /// <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> /// </summary>
/// <param name="key">The unique key for this manager device.</param> public class RoutingFeedbackManager : EssentialsDevice
/// <param name="name">The name of this manager device.</param>
public RoutingFeedbackManager(string key, string name) : base(key, name)
{ {
AddPreActivationAction(SubscribeForMidpointFeedback); /// <summary>
AddPreActivationAction(SubscribeForSinkFeedback); /// 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> /// <summary>
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>. /// Debounce delay in milliseconds
/// </summary> /// </summary>
private void SubscribeForMidpointFeedback() private const long DEBOUNCE_MS = 500;
{
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
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> /// <summary>
/// Subscribes to the InputChanged event on all devices implementing <see cref="IRoutingSinkWithSwitchingWithInputPort"/>. /// Builds a map of which sink devices are downstream of each midpoint device
/// </summary> /// for performance optimization in HandleMidpointUpdate
private void SubscribeForSinkFeedback() /// </summary>
{ private void BuildMidpointSinkMap()
var sinkDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
foreach (var device in sinkDevices)
{ {
device.InputChanged += HandleSinkUpdate; midpointToSinksMap = new Dictionary<string, HashSet<string>>();
}
}
/// <summary> var sinks = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
/// Handles the RouteChanged event from a midpoint device. var midpoints = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
/// 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>();
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> Debug.LogMessage(
/// Handles the InputChanged event from a sink device. Serilog.Events.LogEventLevel.Information,
/// Triggers an update for the specific sink device. "Built midpoint-to-sink map with {count} midpoints",
/// </summary> this,
/// <param name="sender">The sink device that reported an input change.</param> midpointToSinksMap.Count
/// <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;
} }
TieLine firstTieLine; /// <summary>
try /// 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) var tieLine = TieLineCollection.Default.FirstOrDefault(tl =>
{ tl.DestinationPort.Key == sink.CurrentInputPort.Key &&
Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "No tieline found for inputPort {inputPort}. Clearing current source", this, inputPort); 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; 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); visited.Add(tieLine.SourcePort.ParentDevice.Key);
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);
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint) 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); foreach (var inputPort in midpointInputs)
return null; {
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); var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
return null;
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;
}
}
} }
} }

View file

@ -27,16 +27,7 @@ namespace PepperDash.Essentials.Core
/// <summary> /// <summary>
/// Control signal type /// Control signal type
/// </summary> /// </summary>
UsbOutput = 8, Usb = 8,
/// <summary>
/// Control signal type
/// </summary>
UsbInput = 16,
/// <summary>
/// Secondary audio signal type
/// </summary>
SecondaryAudio = 32
} }
} }

View file

@ -179,6 +179,11 @@ public class EssentialsWebApi : EssentialsDevice
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);

View file

@ -0,0 +1,241 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
/// <summary>
/// Handles HTTP requests to retrieve routing devices and tielines information
/// </summary>
public class GetRoutingDevicesAndTieLinesHandler : WebApiBaseRequestHandler
{
/// <summary>
/// Initializes a new instance of the <see cref="GetRoutingDevicesAndTieLinesHandler"/> class.
/// </summary>
public GetRoutingDevicesAndTieLinesHandler() : base(true) { }
/// <summary>
/// Handles the GET request to retrieve routing devices and tielines information
/// </summary>
/// <param name="context"></param>
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
{
/// <summary>
/// Gets or sets the list of routing devices in the system, including their ports information
/// </summary>
[JsonProperty("devices")]
public List<RoutingDeviceInfo> Devices { get; set; }
/// <summary>
/// Gets or sets the list of tielines in the system, including source/destination device and port information
/// </summary>
[JsonProperty("tieLines")]
public List<TieLineInfo> TieLines { get; set; }
}
/// <summary>
/// Represents a routing device with its ports information
/// </summary>
public class RoutingDeviceInfo : IKeyName
{
/// <inheritdoc />
[JsonProperty("key")]
public string Key { get; set; }
/// <inheritdoc />
[JsonProperty("name")]
public string Name { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the device has routing input ports
/// </summary>
[JsonProperty("hasInputs")]
public bool HasInputs { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the device has routing output ports
/// </summary>
[JsonProperty("hasOutputs")]
public bool HasOutputs { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the device has both routing inputs and outputs (e.g., matrix switcher)
/// </summary>
[JsonProperty("hasInputsAndOutputs")]
public bool HasInputsAndOutputs { get; set; }
/// <summary>
/// Gets or sets the list of input ports for the device, if applicable. Null if the device does not have routing inputs.
/// </summary>
[JsonProperty("inputPorts", NullValueHandling = NullValueHandling.Ignore)]
public List<PortInfo> InputPorts { get; set; }
/// <summary>
/// Gets or sets the list of output ports for the device, if applicable. Null if the device does not have routing outputs.
/// </summary>
[JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)]
public List<PortInfo> OutputPorts { get; set; }
}
/// <summary>
/// Represents a routing port with its properties
/// </summary>
public class PortInfo : IKeyed
{
/// <inheritdoc />
[JsonProperty("key")]
public string Key { get; set; }
/// <summary>
/// Gets or sets the signal type of the port (e.g., AudioVideo, Audio, Video, etc.)
/// </summary>
[JsonProperty("signalType")]
public string SignalType { get; set; }
/// <summary>
/// Gets or sets the connection type of the port (e.g., Hdmi, Dvi, Vga, etc.)
/// </summary>
[JsonProperty("connectionType")]
public string ConnectionType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the port is internal
/// </summary>
[JsonProperty("isInternal")]
public bool IsInternal { get; set; }
}
/// <summary>
/// Represents a tieline connection between two ports
/// </summary>
public class TieLineInfo
{
/// <summary>
/// Gets or sets the key of the source device for the tieline connection
/// </summary>
[JsonProperty("sourceDeviceKey")]
public string SourceDeviceKey { get; set; }
/// <summary>
/// Gets or sets the key of the source port for the tieline connection
/// </summary>
[JsonProperty("sourcePortKey")]
public string SourcePortKey { get; set; }
/// <summary>
/// Gets or sets the key of the destination device for the tieline connection
/// </summary>
[JsonProperty("destinationDeviceKey")]
public string DestinationDeviceKey { get; set; }
/// <summary>
/// Gets or sets the key of the destination port for the tieline connection
/// </summary>
[JsonProperty("destinationPortKey")]
public string DestinationPortKey { get; set; }
/// <summary>
/// Gets or sets the signal type of the tieline connection (e.g., AudioVideo, Audio, Video, etc.)
/// </summary>
[JsonProperty("signalType")]
public string SignalType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the tieline connection is internal
/// </summary>
[JsonProperty("isInternal")]
public bool IsInternal { get; set; }
}
}

View file

@ -32,7 +32,7 @@ public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputP
{ {
InputPorts = new RoutingPortCollection<RoutingInputPort>(); InputPorts = new RoutingPortCollection<RoutingInputPort>();
var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this); var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this);
InputPorts.Add(inputPort); InputPorts.Add(inputPort);

View file

@ -12,17 +12,17 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <summary> /// <summary>
/// Represents a IHasCurrentSourceInfoMessenger /// Represents a IHasCurrentSourceInfoMessenger
/// </summary> /// </summary>
public class CurrentSourcesMessenger : MessengerBase public class ICurrentSourcesMessenger : MessengerBase
{ {
private readonly ICurrentSources sourceDevice; private readonly ICurrentSources sourceDevice;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="CurrentSourcesMessenger"/> class. /// Initializes a new instance of the <see cref="ICurrentSourcesMessenger"/> class.
/// </summary> /// </summary>
/// <param name="key">The key.</param> /// <param name="key">The key.</param>
/// <param name="messagePath">The message path.</param> /// <param name="messagePath">The message path.</param>
/// <param name="device">The device.</param> /// <param name="device">The device.</param>
public CurrentSourcesMessenger(string key, string messagePath, ICurrentSources device) : base(key, messagePath, device as IKeyName) public ICurrentSourcesMessenger(string key, string messagePath, ICurrentSources device) : base(key, messagePath, device as IKeyName)
{ {
sourceDevice = device; sourceDevice = device;
} }

View file

@ -10,19 +10,19 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// Facilitates communication of device information by providing mechanisms for status updates and device /// Facilitates communication of device information by providing mechanisms for status updates and device
/// information reporting. /// information reporting.
/// </summary> /// </summary>
/// <remarks>The <see cref="DeviceInfoMessenger"/> class integrates with an <see /// <remarks>The <see cref="IDeviceInfoProviderMessenger"/> class integrates with an <see
/// cref="IDeviceInfoProvider"/> to manage device-specific information. It uses a debounce timer to limit the /// cref="IDeviceInfoProvider"/> to manage device-specific information. It uses a debounce timer to limit the
/// frequency of updates, ensuring efficient communication. The timer is initialized with a 1-second interval and /// frequency of updates, ensuring efficient communication. The timer is initialized with a 1-second interval and
/// is disabled by default. This class also subscribes to device information change events and provides actions for /// is disabled by default. This class also subscribes to device information change events and provides actions for
/// reporting full device status and triggering updates.</remarks> /// reporting full device status and triggering updates.</remarks>
public class DeviceInfoMessenger : MessengerBase public class IDeviceInfoProviderMessenger : MessengerBase
{ {
private readonly IDeviceInfoProvider _deviceInfoProvider; private readonly IDeviceInfoProvider _deviceInfoProvider;
private readonly Timer debounceTimer; private readonly Timer debounceTimer;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DeviceInfoMessenger"/> class, which facilitates communication /// Initializes a new instance of the <see cref="IDeviceInfoProviderMessenger"/> class, which facilitates communication
/// of device information. /// of device information.
/// </summary> /// </summary>
/// <remarks>The messenger uses a debounce timer to limit the frequency of certain operations. The /// <remarks>The messenger uses a debounce timer to limit the frequency of certain operations. The
@ -30,7 +30,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <param name="key">A unique identifier for the messenger instance.</param> /// <param name="key">A unique identifier for the messenger instance.</param>
/// <param name="messagePath">The path used for sending and receiving messages.</param> /// <param name="messagePath">The path used for sending and receiving messages.</param>
/// <param name="device">An implementation of <see cref="IDeviceInfoProvider"/> that provides device-specific information.</param> /// <param name="device">An implementation of <see cref="IDeviceInfoProvider"/> that provides device-specific information.</param>
public DeviceInfoMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device) public IDeviceInfoProviderMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device)
{ {
_deviceInfoProvider = device; _deviceInfoProvider = device;

View file

@ -774,7 +774,7 @@ namespace PepperDash.Essentials
{ {
this.LogVerbose("Adding CurrentSourcesMessenger for {deviceKey}", device.Key); this.LogVerbose("Adding CurrentSourcesMessenger for {deviceKey}", device.Key);
var messenger = new CurrentSourcesMessenger($"{device.Key}-currentSources-{Key}", $"/device/{device.Key}", currentSources); var messenger = new ICurrentSourcesMessenger($"{device.Key}-currentSources-{Key}", $"/device/{device.Key}", currentSources);
AddDefaultDeviceMessenger(messenger); AddDefaultDeviceMessenger(messenger);
@ -803,7 +803,7 @@ namespace PepperDash.Essentials
this.LogVerbose("Adding IHasDeviceInfoMessenger for {deviceKey}", device.Key this.LogVerbose("Adding IHasDeviceInfoMessenger for {deviceKey}", device.Key
); );
var messenger = new DeviceInfoMessenger( var messenger = new IDeviceInfoProviderMessenger(
$"{device.Key}-deviceInfo-{Key}", $"{device.Key}-deviceInfo-{Key}",
$"/device/{device.Key}", $"/device/{device.Key}",
provider provider

View file

@ -140,6 +140,17 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator);
CrestronConsole.AddNewConsoleCommand(ListTieLines,
"listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator);
CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes",
"Visualizes routes by signal type",
ConsoleAccessLevelEnum.AccessOperator);
CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes",
"Visualizes current active routes from DefaultCollection",
ConsoleAccessLevelEnum.AccessOperator);
CrestronConsole.AddNewConsoleCommand(s => CrestronConsole.AddNewConsoleCommand(s =>
{ {
foreach (var tl in TieLineCollection.Default) foreach (var tl in TieLineCollection.Default)
@ -480,6 +491,282 @@ public class ControlSystem : CrestronControlSystem, ILoadConfig
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>