Merge pull request #1374 from PepperDash/tieline-visualisation

tieline visualisation
This commit is contained in:
Neil Dorin 2026-04-30 10:59:50 -06:00 committed by GitHub
commit f31f0611f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2040 additions and 844 deletions

View file

@ -436,14 +436,14 @@ namespace PepperDash.Essentials.Core
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine);
foreach (var routingInputPort in inputPorts)
{
CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, CrestronEnvironment.NewLine);
CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingInputPort.Key, routingInputPort.Type, CrestronEnvironment.NewLine);
}
}
if (outputPorts == null) return;
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Output Ports:{2}", s, outputPorts.Count, CrestronEnvironment.NewLine);
foreach (var routingOutputPort in outputPorts)
{
CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine);
CrestronConsole.ConsoleCommandResponse("key: {0} signalType: {1}{2}", routingOutputPort.Key, routingOutputPort.Type, CrestronEnvironment.NewLine);
}
}

View file

@ -1,11 +1,10 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Crestron.SimplSharpPro.Keypads;
using PepperDash.Essentials.Core.Queues;
using PepperDash.Essentials.Core.Routing;
using Serilog.Events;
using Debug = PepperDash.Core.Debug;
@ -18,6 +17,20 @@ namespace PepperDash.Essentials.Core
/// </summary>
public static class Extensions
{
/// <summary>
/// A collection of RouteDescriptors for each signal type.
/// </summary>
public static readonly Dictionary<eRoutingSignalType, RouteDescriptorCollection> RouteDescriptors = new Dictionary<eRoutingSignalType, RouteDescriptorCollection>()
{
{ eRoutingSignalType.Audio, new RouteDescriptorCollection() },
{ eRoutingSignalType.Video, new RouteDescriptorCollection() },
{ eRoutingSignalType.SecondaryAudio, new RouteDescriptorCollection() },
{ eRoutingSignalType.AudioVideo, new RouteDescriptorCollection() },
{ eRoutingSignalType.UsbInput, new RouteDescriptorCollection() },
{ eRoutingSignalType.UsbOutput, new RouteDescriptorCollection() }
};
/// <summary>
/// Stores pending route requests, keyed by the destination device key.
/// Used primarily to handle routing requests while a device is cooling down.
@ -29,6 +42,108 @@ namespace PepperDash.Essentials.Core
/// </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"
/// Uses ConcurrentDictionary as a thread-safe set (byte value is unused).
/// </summary>
private static readonly ConcurrentDictionary<string, byte> _impossibleRoutes = new ConcurrentDictionary<string, byte>();
/// <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.LogInformation("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.LogInformation("TieLine indexing complete. {0} destination keys, {1} source keys",
_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="sourcePortKey">Source port key</param>
/// <param name="destinationPortKey">Destination port key</param>
/// <param name="type">Signal type</param>
/// <returns>Cache key string</returns>
private static string GetRouteKey(string sourceKey, string destKey, string sourcePortKey, string destinationPortKey, eRoutingSignalType type)
{
return $"{sourceKey}|{destKey}|{sourcePortKey}|{destinationPortKey}|{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.LogInformation("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
@ -38,7 +153,7 @@ namespace PepperDash.Essentials.Core
{
// Remove this line before committing!!!!!
var frame = new StackFrame(1, true);
Debug.LogMessage(LogEventLevel.Information, "ReleaseAndMakeRoute Called from {method} with params {destinationKey}:{sourceKey}:{signalType}:{destinationPortKey}:{sourcePortKey}", frame.GetMethod().Name, destination.Key, source.Key, signalType.ToString(), destinationPortKey, sourcePortKey);
Debug.LogInformation("ReleaseAndMakeRoute Called from {method} with params {destinationKey}:{sourceKey}:{signalType}:{destinationPortKey}:{sourcePortKey}", frame.GetMethod().Name, destination.Key, source.Key, signalType.ToString(), destinationPortKey, sourcePortKey);
var inputPort = string.IsNullOrEmpty(destinationPortKey) ? null : destination.InputPorts.FirstOrDefault(p => p.Key == destinationPortKey);
var outputPort = string.IsNullOrEmpty(sourcePortKey) ? null : source.OutputPorts.FirstOrDefault(p => p.Key == sourcePortKey);
@ -96,13 +211,13 @@ namespace PepperDash.Essentials.Core
/// <param name="destinationKey">destination device key</param>
public static void RemoveRouteRequestForDestination(string destinationKey)
{
Debug.LogMessage(LogEventLevel.Information, "Removing route request for {destination}", null, destinationKey);
Debug.LogInformation("Removing route request for {destination}", destinationKey);
var result = RouteRequests.Remove(destinationKey);
var messageTemplate = result ? "Route Request for {destination} removed" : "Route Request for {destination} not found";
Debug.LogMessage(LogEventLevel.Information, messageTemplate, null, destinationKey);
Debug.LogInformation(messageTemplate, destinationKey);
}
/// <summary>
@ -118,8 +233,8 @@ namespace PepperDash.Essentials.Core
if (!signalType.HasFlag(eRoutingSignalType.AudioVideo) &&
!(signalType.HasFlag(eRoutingSignalType.Video) && signalType.HasFlag(eRoutingSignalType.SecondaryAudio)))
{
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);
var singleTypeRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, signalType);
Debug.LogDebug(destination, "Attempting to build source route from {sourceKey} of type {type}", source.Key, signalType);
if (!destination.GetRouteToSource(source, null, null, signalType, 0, singleTypeRouteDescriptor, destinationPort, sourcePort))
singleTypeRouteDescriptor = null;
@ -127,54 +242,55 @@ namespace PepperDash.Essentials.Core
var routes = singleTypeRouteDescriptor?.Routes ?? new List<RouteSwitchDescriptor>();
foreach (var route in routes)
{
Debug.LogMessage(LogEventLevel.Verbose, "Route for device: {route}", destination, route.ToString());
Debug.LogVerbose(destination, "Route for device: {route}", route.ToString());
}
return (singleTypeRouteDescriptor, null);
}
// otherwise, audioVideo needs to be handled as two steps.
Debug.LogMessage(LogEventLevel.Debug, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination, source.Key, signalType);
Debug.LogDebug(destination, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", source.Key, signalType);
RouteDescriptor audioRouteDescriptor;
if (signalType.HasFlag(eRoutingSignalType.SecondaryAudio))
{
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.SecondaryAudio);
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.SecondaryAudio);
}
else
{
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Audio);
audioRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Audio);
}
var audioSuccess = destination.GetRouteToSource(source, null, null, signalType.HasFlag(eRoutingSignalType.SecondaryAudio) ? eRoutingSignalType.SecondaryAudio : eRoutingSignalType.Audio, 0, audioRouteDescriptor, destinationPort, sourcePort);
if (!audioSuccess)
Debug.LogMessage(LogEventLevel.Debug, "Cannot find audio route to {0}", destination, source.Key);
Debug.LogDebug(destination, "Cannot find audio route to {0}", source.Key);
var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, eRoutingSignalType.Video);
var videoRouteDescriptor = new RouteDescriptor(source, destination, destinationPort, sourcePort, eRoutingSignalType.Video);
var videoSuccess = destination.GetRouteToSource(source, null, null, eRoutingSignalType.Video, 0, videoRouteDescriptor, destinationPort, sourcePort);
if (!videoSuccess)
Debug.LogMessage(LogEventLevel.Debug, "Cannot find video route to {0}", destination, source.Key);
Debug.LogDebug(destination, "Cannot find video route to {0}", source.Key);
foreach (var route in audioRouteDescriptor.Routes)
{
Debug.LogMessage(LogEventLevel.Verbose, "Audio route for device: {route}", destination, route.ToString());
Debug.LogVerbose(destination, "Audio route for device: {route}", route.ToString());
}
foreach (var route in videoRouteDescriptor.Routes)
{
Debug.LogMessage(LogEventLevel.Verbose, "Video route for device: {route}", destination, route.ToString());
Debug.LogVerbose(destination, "Video route for device: {route}", route.ToString());
}
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>
@ -190,8 +306,8 @@ namespace PepperDash.Essentials.Core
{
if (destination == null) throw new ArgumentNullException(nameof(destination));
if (source == null) throw new ArgumentNullException(nameof(source));
if (destinationPort == null) Debug.LogMessage(LogEventLevel.Information, "Destination port is null");
if (sourcePort == null) Debug.LogMessage(LogEventLevel.Information, "Source port is null");
if (destinationPort == null) Debug.LogDebug("Destination port is null");
if (sourcePort == null) Debug.LogDebug("Source port is null");
var routeRequest = new RouteRequest
{
@ -213,7 +329,7 @@ namespace PepperDash.Essentials.Core
RouteRequests[destination.Key] = routeRequest;
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down and already has a routing request stored. Storing new route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
Debug.LogInformation("Device: {destination} is cooling down and already has a routing request stored. Storing new route request to route to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
return;
}
@ -225,7 +341,7 @@ namespace PepperDash.Essentials.Core
RouteRequests.Add(destination.Key, routeRequest);
Debug.LogMessage(LogEventLevel.Information, "Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", null, destination.Key, routeRequest.Source.Key);
Debug.LogInformation("Device: {destination} is cooling down. Storing route request to route to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
return;
}
@ -237,7 +353,7 @@ namespace PepperDash.Essentials.Core
RouteRequests.Remove(destination.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);
Debug.LogInformation("Device: {destination} is NOT cooling down. Removing stored route request and routing to source key: {sourceKey}", destination.Key, routeRequest.Source.Key);
}
routeRequestQueue.Enqueue(new ReleaseRouteQueueItem(ReleaseRouteInternal, destination, destinationPort?.Key ?? string.Empty, false));
@ -245,6 +361,90 @@ namespace PepperDash.Essentials.Core
routeRequestQueue.Enqueue(new RouteRequestQueueItem(RunRouteRequest, routeRequest));
}
/// <summary>
/// Maps destination input ports to source output ports for all routing devices.
/// </summary>
public static void MapDestinationsToSources()
{
try
{
// Index TieLines before mapping if not already done
if (_tieLinesByDestination == null || _tieLinesBySource == null)
{
IndexTieLines();
}
var sinks = DeviceManager.AllDevices.OfType<IRoutingInputs>().Where(d => !(d is IRoutingInputsOutputs));
var sources = DeviceManager.AllDevices.OfType<IRoutingOutputs>().Where(d => !(d is IRoutingInputsOutputs));
foreach (var sink in sinks)
{
foreach (var source in sources)
{
foreach (var inputPort in sink.InputPorts)
{
foreach (var outputPort in source.OutputPorts)
{
var (audioOrSingleRoute, videoRoute) = sink.GetRouteToSource(source, inputPort.Type, inputPort, outputPort);
if (audioOrSingleRoute == null && videoRoute == null)
{
continue;
}
if (audioOrSingleRoute != null)
{
// Only add routes that have actual switching steps
if (audioOrSingleRoute.Routes == null || audioOrSingleRoute.Routes.Count == 0)
{
continue;
}
// Add to the appropriate collection(s) based on signal type
// Note: A single route descriptor with combined flags (e.g., AudioVideo) will be added once per matching signal type
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Audio))
{
RouteDescriptors[eRoutingSignalType.Audio].AddRouteDescriptor(audioOrSingleRoute);
}
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Video))
{
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(audioOrSingleRoute);
}
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.SecondaryAudio))
{
RouteDescriptors[eRoutingSignalType.SecondaryAudio].AddRouteDescriptor(audioOrSingleRoute);
}
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.UsbInput))
{
RouteDescriptors[eRoutingSignalType.UsbInput].AddRouteDescriptor(audioOrSingleRoute);
}
if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.UsbOutput))
{
RouteDescriptors[eRoutingSignalType.UsbOutput].AddRouteDescriptor(audioOrSingleRoute);
}
}
if (videoRoute != null)
{
// Only add routes that have actual switching steps
if (videoRoute.Routes == null || videoRoute.Routes.Count == 0)
{
continue;
}
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute);
}
}
}
}
}
}
catch (Exception ex)
{
Debug.LogError("Exception mapping routes: {exception}", ex.Message);
Debug.LogDebug(ex, "Stack Trace: ");
}
}
/// <summary>
/// Executes the actual routing based on a <see cref="RouteRequest"/>.
/// Finds the route path, adds it to the collection, and executes the switches.
@ -257,7 +457,54 @@ namespace PepperDash.Essentials.Core
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) &&
(request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.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) &&
(request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.Key));
}
}
else
{
// For single signal type routes
var signalTypeToCheck = request.SignalType.HasFlag(eRoutingSignalType.SecondaryAudio)
? eRoutingSignalType.SecondaryAudio
: request.SignalType;
if (RouteDescriptors.TryGetValue(signalTypeToCheck, out RouteDescriptorCollection collection))
{
audioOrSingleRoute = collection.Descriptors.FirstOrDefault(d =>
d.Source.Key == request.Source.Key &&
d.Destination.Key == request.Destination.Key &&
(request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key) &&
(request.SourcePort == null || d.OutputPort?.Key == request.SourcePort.Key));
}
}
// If no pre-loaded route found, build it dynamically
if (audioOrSingleRoute == null && videoRoute == null)
{
Debug.LogDebug(request.Destination, "No pre-loaded route found, building dynamically");
(audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort);
}
if (audioOrSingleRoute == null && videoRoute == null)
return;
@ -269,14 +516,15 @@ namespace PepperDash.Essentials.Core
RouteDescriptorCollection.DefaultCollection.AddRouteDescriptor(videoRoute);
}
Debug.LogMessage(LogEventLevel.Verbose, "Executing full route", request.Destination);
Debug.LogVerbose(request.Destination, "Executing full route");
audioOrSingleRoute.ExecuteRoutes();
videoRoute?.ExecuteRoutes();
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Exception Running Route Request {request}", null, request);
Debug.LogError("Exception Running Route Request {request}: {exception}", request, ex.Message);
Debug.LogDebug(ex, "Stack Trace: ");
}
}
@ -290,7 +538,7 @@ namespace PepperDash.Essentials.Core
{
try
{
Debug.LogMessage(LogEventLevel.Information, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
Debug.LogInformation(destination, "Release route for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
if (RouteRequests.TryGetValue(destination.Key, out RouteRequest existingRequest) && destination is IWarmingCooling)
{
@ -304,13 +552,14 @@ namespace PepperDash.Essentials.Core
var current = RouteDescriptorCollection.DefaultCollection.RemoveRouteDescriptor(destination, inputPortKey);
if (current != null)
{
Debug.LogMessage(LogEventLevel.Information, "Releasing current route: {0}", destination, current.Source.Key);
Debug.LogInformation(destination, "Releasing current route: {0}", current.Source.Key);
current.ReleaseRoutes(clearRoute);
}
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Exception releasing route for '{destination}':'{inputPortKey}'", null, destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
Debug.LogError("Exception releasing route for '{destination}':'{inputPortKey}': {exception}", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey, ex.Message);
Debug.LogDebug(ex, "Stack Trace: ");
}
}
@ -321,13 +570,13 @@ namespace PepperDash.Essentials.Core
/// </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="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>
/// <param name="routeTable">The RouteDescriptor being populated as the route is discovered</param>
/// <param name="outputPortToUse">The RoutingOutputPort to use for the route</param>
/// <param name="sourcePort">The specific source output port to use (optional)</param>
/// <param name="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
/// <param name="sourcePort">The source output port (optional)</param>
/// <returns>true if source is hit</returns>
private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
RoutingOutputPort outputPortToUse, List<IRoutingInputsOutputs> alreadyCheckedDevices,
@ -335,42 +584,54 @@ namespace PepperDash.Essentials.Core
{
cycle++;
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());
// Check if this route has already been determined to be impossible
var routeKey = GetRouteKey(source.Key, destination.Key, sourcePort?.Key ?? "auto", destinationPort?.Key ?? "auto", signalType);
if (_impossibleRoutes.ContainsKey(routeKey))
{
Debug.LogVerbose("Route {0} is cached as impossible, skipping", routeKey);
return false;
}
Debug.LogVerbose("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
@ -379,7 +640,7 @@ namespace PepperDash.Essentials.Core
}
else // no direct-connect. Walk back devices.
{
Debug.LogMessage(LogEventLevel.Verbose, "is not directly connected to {sourceKey}. Walking down tie lines", destination, source.Key);
Debug.LogVerbose(destination, "is not directly connected to {sourceKey}. Walking down tie lines", source.Key);
// No direct tie? Run back out on the inputs' attached devices...
// Only the ones that are routing devices
@ -397,13 +658,13 @@ namespace PepperDash.Essentials.Core
// Check if this previous device has already been walked
if (alreadyCheckedDevices.Contains(midpointDevice))
{
Debug.LogMessage(LogEventLevel.Verbose, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", destination, midpointDevice.Key, destination.Key);
Debug.LogVerbose(destination, "Skipping input {midpointDeviceKey} on {destinationKey}, this was already checked", midpointDevice.Key, destination.Key);
continue;
}
var midpointOutputPort = tieLine.SourcePort;
Debug.LogMessage(LogEventLevel.Verbose, "Trying to find route on {midpointDeviceKey}", destination, midpointDevice.Key);
Debug.LogVerbose(destination, "Trying to find route on {midpointDeviceKey}", midpointDevice.Key);
// haven't seen this device yet. Do it. Pass the output port to the next
// level to enable switching on success
@ -412,9 +673,9 @@ namespace PepperDash.Essentials.Core
if (upstreamRoutingSuccess)
{
Debug.LogMessage(LogEventLevel.Verbose, "Upstream device route found", destination);
Debug.LogMessage(LogEventLevel.Verbose, "Route found on {midpointDeviceKey}", destination, midpointDevice.Key);
Debug.LogMessage(LogEventLevel.Verbose, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", destination, tieLine.SourcePort, tieLine.DestinationPort);
Debug.LogVerbose(destination, "Upstream device route found");
Debug.LogVerbose(destination, "Route found on {midpointDeviceKey}", midpointDevice.Key);
Debug.LogVerbose(destination, "TieLine: SourcePort: {SourcePort} DestinationPort: {DestinationPort}", tieLine.SourcePort, tieLine.DestinationPort);
goodInputPort = tieLine.DestinationPort;
break; // Stop looping the inputs in this cycle
}
@ -424,7 +685,11 @@ namespace PepperDash.Essentials.Core
if (goodInputPort == null)
{
Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key);
Debug.LogVerbose(destination, "No route found to {0}", source.Key);
// Cache this as an impossible route
_impossibleRoutes.TryAdd(routeKey, 0);
return false;
}
@ -440,7 +705,7 @@ namespace PepperDash.Essentials.Core
routeTable.Routes.Add(new RouteSwitchDescriptor(outputPortToUse, goodInputPort));
}
else // device is merely IRoutingInputOutputs
Debug.LogMessage(LogEventLevel.Verbose, "No routing. Passthrough device", destination);
Debug.LogVerbose(destination, "No routing. Passthrough device");
return true;
}

View file

@ -20,22 +20,27 @@ namespace PepperDash.Essentials.Core
public IRoutingInputs Destination { get; private set; }
/// <summary>
/// Gets or sets the InputPort
/// The InputPort on the destination device for this route, if applicable. May be null if the route is not for a specific input port.
/// </summary>
public RoutingInputPort InputPort { get; private set; }
/// <summary>
/// Gets or sets the Source
/// Gets the source device (sink or midpoint) for the route.
/// </summary>
public IRoutingOutputs Source { get; private set; }
/// <summary>
/// Gets or sets the SignalType
/// Gets the OutputPort on the source device for this route, if applicable. May be null if the route is not for a specific output port.
/// </summary>
public RoutingOutputPort OutputPort { get; private set; }
/// <summary>
/// Gets the signal type for this route.
/// </summary>
public eRoutingSignalType SignalType { get; private set; }
/// <summary>
/// Gets or sets the Routes
/// Gets the collection of route switch descriptors for this route.
/// </summary>
public List<RouteSwitchDescriptor> Routes { get; private set; }
@ -56,11 +61,24 @@ namespace PepperDash.Essentials.Core
/// <param name="destination">The destination device.</param>
/// <param name="inputPort">The destination input port (optional).</param>
/// <param name="signalType">The signal type for this route.</param>
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType)
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, eRoutingSignalType signalType) : this(source, destination, inputPort, null, signalType)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RouteDescriptor"/> class for a route with specific destination input and source output ports.
/// </summary>
/// <param name="source"></param>
/// <param name="destination"></param>
/// <param name="inputPort"></param>
/// <param name="outputPort"></param>
/// <param name="signalType"></param>
public RouteDescriptor(IRoutingOutputs source, IRoutingInputs destination, RoutingInputPort inputPort, RoutingOutputPort outputPort, eRoutingSignalType signalType)
{
Destination = destination;
InputPort = inputPort;
Source = source;
OutputPort = outputPort;
SignalType = signalType;
Routes = new List<RouteSwitchDescriptor>();
}
@ -72,7 +90,7 @@ namespace PepperDash.Essentials.Core
{
foreach (var route in Routes)
{
Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString());
Debug.LogVerbose("ExecuteRoutes: {0}", route.ToString());
if (route.SwitchingDevice is IRoutingSinkWithSwitching sink)
{
@ -86,7 +104,7 @@ namespace PepperDash.Essentials.Core
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);
Debug.LogVerbose("Output port {0} routing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
}
}
}
@ -95,15 +113,15 @@ namespace PepperDash.Essentials.Core
/// Releases the usage tracking for the route and optionally clears the route on the switching devices.
/// </summary>
/// <param name="clearRoute">If true, attempts to clear the route on the switching devices (e.g., set input to null/0).</param>
public void ReleaseRoutes(bool clearRoute = false)
{
foreach (var route in Routes.Where(r => r.SwitchingDevice is IRouting))
{
if (route.SwitchingDevice is IRouting switchingDevice)
{
if(clearRoute)
if (clearRoute)
{
try
{
@ -112,6 +130,7 @@ namespace PepperDash.Essentials.Core
catch (Exception e)
{
Debug.LogError("Error executing switch: {exception}", e.Message);
Debug.LogDebug(e, "Stack Trace: ");
}
}
@ -123,11 +142,11 @@ namespace PepperDash.Essentials.Core
if (route.OutputPort.InUseTracker != null)
{
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);
Debug.LogVerbose("Port {0} releasing. Count={1}", route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
}
else
{
Debug.LogMessage(LogEventLevel.Error, "InUseTracker is null for OutputPort {0}", null, route.OutputPort.Key);
Debug.LogVerbose("InUseTracker is null for OutputPort {0}", route.OutputPort.Key);
}
}
}
@ -137,98 +156,11 @@ namespace PepperDash.Essentials.Core
/// Returns a string representation of the route descriptor, including source, destination, and individual route steps.
/// </summary>
/// <returns>A string describing the route.</returns>
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>
/// <summary>
/// Represents a RouteDescriptor
/// </summary>
public class RouteDescriptor<TInputSelector, TOutputSelector>
{
/// <summary>
/// Gets or sets the Destination
/// </summary>
public IRoutingInputs<TInputSelector> Destination { get; private set; }
/// <summary>
/// Gets or sets the Source
/// </summary>
public IRoutingOutputs<TOutputSelector> Source { get; private set; }
/// <summary>
/// Gets or sets the SignalType
/// </summary>
public eRoutingSignalType SignalType { get; private set; }
public List<RouteSwitchDescriptor<TInputSelector, TOutputSelector>> Routes { get; private set; }
public RouteDescriptor(IRoutingOutputs<TOutputSelector> source, IRoutingInputs<TInputSelector> destination, eRoutingSignalType signalType)
{
Destination = destination;
Source = source;
SignalType = signalType;
Routes = new List<RouteSwitchDescriptor<TInputSelector, TOutputSelector>>();
}
/// <summary>
/// ExecuteRoutes method
/// </summary>
public void ExecuteRoutes()
{
foreach (var route in Routes)
{
Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString());
if (route.SwitchingDevice is IRoutingSinkWithSwitching<TInputSelector> sink)
{
sink.ExecuteSwitch(route.InputPort.Selector);
continue;
}
if (route.SwitchingDevice is IRouting switchingDevice)
{
switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType);
route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType);
Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
}
}
}
/// <summary>
/// ReleaseRoutes method
/// </summary>
public void ReleaseRoutes()
{
foreach (var route in Routes)
{
if (route.SwitchingDevice is IRouting<TInputSelector, TOutputSelector>)
{
// Pull the route from the port. Whatever is watching the output's in use tracker is
// responsible for responding appropriately.
route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType);
Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
}
}
}
/// <summary>
/// ToString method
/// </summary>
/// <inheritdoc />
public override string ToString()
{
var routesText = Routes.Select(r => r.ToString()).ToArray();
return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText));
}
}*/
}

View file

@ -1,7 +1,7 @@
using PepperDash.Core;
using Serilog.Events;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using PepperDash.Core;
using Serilog.Events;
namespace PepperDash.Essentials.Core
@ -12,7 +12,7 @@ namespace PepperDash.Essentials.Core
public class RouteDescriptorCollection
{
/// <summary>
/// DefaultCollection static property
/// Gets the default collection of RouteDescriptors.
/// </summary>
public static RouteDescriptorCollection DefaultCollection
{
@ -27,6 +27,11 @@ namespace PepperDash.Essentials.Core
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
@ -40,13 +45,34 @@ namespace PepperDash.Essentials.Core
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)) &&
((t.OutputPort == null && descriptor.OutputPort == null) ||
(t.OutputPort != null && descriptor.OutputPort != null && t.OutputPort.Key == descriptor.OutputPort.Key)));
if (existingRoute != null)
{
Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination,
"Route to [{0}] already exists in global routes table", descriptor?.Source?.Key);
Debug.LogInformation(descriptor.Destination,
"Route from {source}:{outputPort} to {destination}:{inputPort} ({signalType}) already exists in this collection",
descriptor?.Source?.Key,
descriptor?.OutputPort?.Key ?? "auto",
descriptor?.Destination?.Key,
descriptor?.InputPort?.Key ?? "auto",
descriptor?.SignalType
);
return;
}
Debug.LogVerbose("Adding route descriptor: {source}:{outputPort} -> {destination}:{inputPort} ({signalType})",
descriptor?.Source?.Key,
descriptor?.OutputPort?.Key ?? "auto",
descriptor?.Destination?.Key,
descriptor?.InputPort?.Key ?? "auto",
descriptor?.SignalType);
RouteDescriptors.Add(descriptor);
}
@ -61,11 +87,11 @@ namespace PepperDash.Essentials.Core
}
/// <summary>
/// Gets the RouteDescriptor for a destination and input port key. Returns null if no matching RouteDescriptor exists.
/// Gets the route descriptor for a specific destination and input port
/// </summary>
/// <param name="destination"></param>
/// <param name="inputPortKey"></param>
/// <returns></returns>
/// <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);
@ -82,7 +108,7 @@ namespace PepperDash.Essentials.Core
{
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)
@ -93,70 +119,4 @@ namespace PepperDash.Essentials.Core
return descr;
}
}
/*/// <summary>
/// A collection of RouteDescriptors - typically the static DefaultCollection is used
/// </summary>
/// <summary>
/// Represents a RouteDescriptorCollection
/// </summary>
public class RouteDescriptorCollection<TInputSelector, TOutputSelector>
{
public static RouteDescriptorCollection<TInputSelector, TOutputSelector> DefaultCollection
{
get
{
if (_DefaultCollection == null)
_DefaultCollection = new RouteDescriptorCollection<TInputSelector, TOutputSelector>();
return _DefaultCollection;
}
}
private static RouteDescriptorCollection<TInputSelector, TOutputSelector> _DefaultCollection;
private readonly List<RouteDescriptor> RouteDescriptors = new List<RouteDescriptor>();
/// <summary>
/// Adds a RouteDescriptor to the list. If an existing RouteDescriptor for the
/// destination exists already, it will not be added - in order to preserve
/// proper route releasing.
/// </summary>
/// <param name="descriptor"></param>
/// <summary>
/// AddRouteDescriptor method
/// </summary>
public void AddRouteDescriptor(RouteDescriptor descriptor)
{
if (RouteDescriptors.Any(t => t.Destination == descriptor.Destination))
{
Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination,
"Route to [{0}] already exists in global routes table", descriptor.Source.Key);
return;
}
RouteDescriptors.Add(descriptor);
}
/// <summary>
/// Gets the RouteDescriptor for a destination
/// </summary>
/// <returns>null if no RouteDescriptor for a destination exists</returns>
/// <summary>
/// GetRouteDescriptorForDestination method
/// </summary>
public RouteDescriptor GetRouteDescriptorForDestination(IRoutingInputs<TInputSelector> destination)
{
return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination);
}
/// <summary>
/// Returns the RouteDescriptor for a given destination AND removes it from collection.
/// Returns null if no route with the provided destination exists.
/// </summary>
public RouteDescriptor RemoveRouteDescriptor(IRoutingInputs<TInputSelector> destination)
{
var descr = GetRouteDescriptorForDestination(destination);
if (descr != null)
RouteDescriptors.Remove(descr);
return descr;
}
}*/
}

View file

@ -4,96 +4,51 @@
/// 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>
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port).
/// </summary>
/// <param name="inputPort">The input port being switched to.</param>
public RouteSwitchDescriptor(RoutingInputPort inputPort)
{
InputPort = inputPort;
}
/// <summary>
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for matrix switchers.
/// </summary>
/// <param name="outputPort">The output port being switched from.</param>
/// <param name="inputPort">The input port being switched to.</param>
public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort)
{
InputPort = inputPort;
OutputPort = outputPort;
}
/// <summary>
/// Returns a string representation of the route switch descriptor.
/// </summary>
/// <returns>A string describing the switch operation.</returns>
/// <inheritdoc />
public override string ToString()
{
if (SwitchingDevice is IRouting)
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}";
else
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}";
}
}
/*/// <summary>
/// Represents an individual link for a route
/// </summary>
/// <summary>
/// Represents a RouteSwitchDescriptor
/// </summary>
public class RouteSwitchDescriptor<TInputSelector, TOutputSelector>
{
/// <summary>
/// Gets or sets the SwitchingDevice
/// </summary>
public IRoutingInputs<TInputSelector> SwitchingDevice { get { return InputPort.ParentDevice; } }
public IRoutingInputs SwitchingDevice { get { return InputPort?.ParentDevice; } }
/// <summary>
/// Gets or sets the OutputPort
/// The output port being switched from (relevant for matrix switchers). Null for sink devices.
/// </summary>
public RoutingOutputPort<TOutputSelector> OutputPort { get; set; }
public RoutingOutputPort OutputPort { get; set; }
/// <summary>
/// Gets or sets the InputPort
/// The input port being switched to.
/// </summary>
public RoutingInputPort<TInputSelector> InputPort { get; set; }
public RoutingInputPort InputPort { get; set; }
public RouteSwitchDescriptor(RoutingInputPort<TInputSelector> inputPort)
/// <summary>
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for sink devices (no output port).
/// </summary>
/// <param name="inputPort">The input port being switched to.</param>
public RouteSwitchDescriptor(RoutingInputPort inputPort)
{
InputPort = inputPort;
}
public RouteSwitchDescriptor(RoutingOutputPort<TOutputSelector> outputPort, RoutingInputPort<TInputSelector> inputPort)
/// <summary>
/// Initializes a new instance of the <see cref="RouteSwitchDescriptor"/> class for matrix switchers.
/// </summary>
/// <param name="outputPort">The output port being switched from.</param>
/// <param name="inputPort">The input port being switched to.</param>
public RouteSwitchDescriptor(RoutingOutputPort outputPort, RoutingInputPort inputPort)
{
InputPort = inputPort;
OutputPort = outputPort;
}
/// <summary>
/// ToString method
/// 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 string.Format("{0} switches output '{1}' to input '{2}'", SwitchingDevice.Key, OutputPort.Selector, InputPort.Selector);
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches output {(OutputPort != null ? OutputPort.Key : "No output port")} to input {(InputPort != null ? InputPort.Key : "No input port")}";
else
return string.Format("{0} switches to input '{1}'", SwitchingDevice.Key, InputPort.Selector);
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}";
}
}*/
}
}

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using PepperDash.Core;
using PepperDash.Essentials.Core.Config;
@ -11,6 +13,26 @@ namespace PepperDash.Essentials.Core.Routing
/// </summary>
public class RoutingFeedbackManager : EssentialsDevice
{
/// <summary>
/// Maps midpoint device keys to the set of sink device keys that are downstream
/// </summary>
private Dictionary<string, HashSet<string>> midpointToSinksMap;
/// <summary>
/// Debounce timers for each sink device to prevent rapid successive updates
/// </summary>
private readonly Dictionary<string, Timer> updateTimers = new Dictionary<string, Timer>();
/// <summary>
/// Lock object protecting all access to <see cref="updateTimers"/>.
/// </summary>
private readonly object _timerLock = new object();
/// <summary>
/// Debounce delay in milliseconds
/// </summary>
private const long DEBOUNCE_MS = 500;
/// <summary>
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
/// </summary>
@ -19,10 +41,99 @@ namespace PepperDash.Essentials.Core.Routing
public RoutingFeedbackManager(string key, string name)
: base(key, name)
{
AddPreActivationAction(BuildMidpointSinkMap);
AddPreActivationAction(SubscribeForMidpointFeedback);
AddPreActivationAction(SubscribeForSinkFeedback);
}
/// <summary>
/// Builds a map of which sink devices are downstream of each midpoint device
/// for performance optimization in HandleMidpointUpdate
/// </summary>
private void BuildMidpointSinkMap()
{
midpointToSinksMap = new Dictionary<string, HashSet<string>>();
var sinks = DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
foreach (var sink in sinks)
{
if (sink.CurrentInputPort == null)
continue;
// Find all upstream midpoints for this sink
var upstreamMidpoints = GetUpstreamMidpoints(sink);
foreach (var midpointKey in upstreamMidpoints)
{
if (!midpointToSinksMap.ContainsKey(midpointKey))
midpointToSinksMap[midpointKey] = new HashSet<string>();
midpointToSinksMap[midpointKey].Add(sink.Key);
}
}
Debug.LogMessage(
Serilog.Events.LogEventLevel.Information,
"Built midpoint-to-sink map with {count} midpoints",
this,
midpointToSinksMap.Count
);
}
/// <summary>
/// Gets all upstream midpoint device keys for a given sink
/// </summary>
private HashSet<string> GetUpstreamMidpoints(IRoutingSinkWithSwitchingWithInputPort sink)
{
var result = new HashSet<string>();
var visited = new HashSet<string>();
if (sink.CurrentInputPort == null)
return result;
var tieLine = TieLineCollection.Default.FirstOrDefault(tl =>
tl.DestinationPort.Key == sink.CurrentInputPort.Key &&
tl.DestinationPort.ParentDevice.Key == sink.CurrentInputPort.ParentDevice.Key);
if (tieLine == null)
return result;
TraceUpstreamMidpoints(tieLine, result, visited);
return result;
}
/// <summary>
/// Recursively traces upstream to find all midpoint devices
/// </summary>
private void TraceUpstreamMidpoints(TieLine tieLine, HashSet<string> midpoints, HashSet<string> visited)
{
if (tieLine == null || visited.Contains(tieLine.SourcePort.ParentDevice.Key))
return;
visited.Add(tieLine.SourcePort.ParentDevice.Key);
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
{
midpoints.Add(midpoint.Key);
// Find upstream TieLines connected to this midpoint's inputs
var midpointInputs = (midpoint as IRoutingInputs)?.InputPorts;
if (midpointInputs != null)
{
foreach (var inputPort in midpointInputs)
{
var upstreamTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
tl.DestinationPort.Key == inputPort.Key &&
tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key);
if (upstreamTieLine != null)
TraceUpstreamMidpoints(upstreamTieLine, midpoints, visited);
}
}
}
}
/// <summary>
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
/// </summary>
@ -52,7 +163,7 @@ namespace PepperDash.Essentials.Core.Routing
/// <summary>
/// Handles the RouteChanged event from a midpoint device.
/// Triggers an update for all sink devices.
/// Only triggers updates for sink devices that are downstream of this midpoint.
/// </summary>
/// <param name="midpoint">The midpoint device that reported a route change.</param>
/// <param name="newRoute">The descriptor of the new route.</param>
@ -63,12 +174,33 @@ namespace PepperDash.Essentials.Core.Routing
{
try
{
var devices =
DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
foreach (var device in devices)
// Only update affected sinks (performance optimization)
if (midpointToSinksMap != null && midpointToSinksMap.TryGetValue(midpoint.Key, out var affectedSinkKeys))
{
UpdateDestination(device, device.CurrentInputPort);
Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"Midpoint {midpoint} changed, updating {count} downstream sinks",
this,
midpoint.Key,
affectedSinkKeys.Count
);
foreach (var sinkKey in affectedSinkKeys)
{
if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithSwitchingWithInputPort sink)
{
UpdateDestination(sink, sink.CurrentInputPort);
}
}
}
else
{
Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"Midpoint {midpoint} changed but has no downstream sinks in map",
this,
midpoint.Key
);
}
}
catch (Exception ex)
@ -83,9 +215,46 @@ namespace PepperDash.Essentials.Core.Routing
}
}
/// <summary>
/// Removes a sink from every midpoint set in the map and re-adds it based on its
/// current input port. Call this whenever a sink's selected input changes so that
/// HandleMidpointUpdate always sees an up-to-date downstream set.
/// </summary>
private void RebuildMapForSink(IRoutingSinkWithSwitchingWithInputPort sink)
{
if (midpointToSinksMap == null)
return;
// Remove this sink from all existing midpoint sets
foreach (var set in midpointToSinksMap.Values)
set.Remove(sink.Key);
// Drop any midpoint entries that are now empty
var emptyKeys = midpointToSinksMap
.Where(kvp => kvp.Value.Count == 0)
.Select(kvp => kvp.Key)
.ToList();
foreach (var k in emptyKeys)
midpointToSinksMap.Remove(k);
// Re-add the sink under every midpoint that is upstream of its new input
if (sink.CurrentInputPort == null)
return;
var upstreamMidpoints = GetUpstreamMidpoints(sink);
foreach (var midpointKey in upstreamMidpoints)
{
if (!midpointToSinksMap.ContainsKey(midpointKey))
midpointToSinksMap[midpointKey] = new HashSet<string>();
midpointToSinksMap[midpointKey].Add(sink.Key);
}
}
/// <summary>
/// Handles the InputChanged event from a sink device.
/// Triggers an update for the specific sink device.
/// Updates the midpoint-to-sink map for the new input path, then triggers
/// a source-info update for the sink.
/// </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>
@ -96,6 +265,10 @@ namespace PepperDash.Essentials.Core.Routing
{
try
{
// Keep the map current so HandleMidpointUpdate can find this sink
if (sender is IRoutingSinkWithSwitchingWithInputPort sinkWithInputPort)
RebuildMapForSink(sinkWithInputPort);
UpdateDestination(sender, currentInputPort);
}
catch (Exception ex)
@ -113,6 +286,7 @@ namespace PepperDash.Essentials.Core.Routing
/// <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>
@ -120,6 +294,76 @@ namespace PepperDash.Essentials.Core.Routing
IRoutingSinkWithSwitching destination,
RoutingInputPort inputPort
)
{
if (destination == null)
return;
var key = destination.Key;
// Cancel and replace any existing timer under the lock so no callback
// can race with us while we swap the entry.
Timer timerToDispose = null;
Timer newTimer = null;
newTimer = new Timer(DEBOUNCE_MS) { AutoReset = false };
newTimer.Elapsed += (s, 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
{
// Remove the entry first so a concurrent UpdateDestination call
// cannot re-dispose whatever timer we're about to dispose.
Timer selfTimer = null;
lock (_timerLock)
{
if (updateTimers.TryGetValue(key, out var current) && ReferenceEquals(current, newTimer))
{
selfTimer = current;
updateTimers.Remove(key);
}
}
selfTimer?.Dispose();
}
};
lock (_timerLock)
{
if (updateTimers.TryGetValue(key, out var existingTimer))
timerToDispose = existingTimer;
updateTimers[key] = newTimer;
}
// Dispose the old timer outside the lock to avoid holding the lock during disposal.
// Dispose implicitly stops the timer, preventing its Elapsed event from firing.
timerToDispose?.Dispose();
// Start after the lock is released so the Elapsed callback cannot deadlock
// trying to acquire _timerLock while we still hold it.
newTimer.Start();
}
/// <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,
@ -206,7 +450,8 @@ namespace PepperDash.Essentials.Core.Routing
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Error getting sourceTieLine: {Exception}", this, ex);
Debug.LogError(this, "Error getting sourceTieLine: {message}", ex.Message);
Debug.LogDebug(ex, "StackTrace: ");
return;
}
@ -230,6 +475,11 @@ namespace PepperDash.Essentials.Core.Routing
return roomDefaultDisplay.DefaultDisplay.Key == destination.Key;
}
if (ConfigReader.ConfigObject.GetDestinationListForKey(r.DestinationListKey)?.FirstOrDefault(d => d.Value.SinkKey == destination.Key) != null)
{
return true;
}
return false;
}
);
@ -251,10 +501,8 @@ namespace PepperDash.Essentials.Core.Routing
if (sourceList == null)
{
Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
Debug.LogDebug(this,
"No source list found for source list key {key}. Unable to find source for tieLine {sourceTieLine}",
this,
room.SourceListKey,
sourceTieLine
);
@ -283,10 +531,8 @@ namespace PepperDash.Essentials.Core.Routing
if (source == null)
{
Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
Debug.LogDebug(this,
"No source found for device {key}. Creating transient source for {destination}",
this,
sourceTieLine.SourcePort.ParentDevice.Key,
destination
);
@ -309,105 +555,92 @@ namespace PepperDash.Essentials.Core.Routing
}
/// <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.
/// 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)
{
TieLine nextTieLine = null;
try
{
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine);
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink))
{
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint);
Debug.LogDebug(this,
"TieLine destination {device} is not IRoutingInputs",
tieLine.DestinationPort.ParentDevice.Key
);
return null;
}
if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0)
// 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,
eRoutingSignalType.SecondaryAudio,
eRoutingSignalType.UsbInput,
eRoutingSignalType.UsbOutput
};
foreach (var signalType in signalTypes)
{
if (!tieLine.Type.HasFlag(signalType))
continue;
foreach (var source in sources)
{
Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"Midpoint {midpointKey} has no routes",
this,
midpoint.Key
// Use the optimized route discovery with loop protection
var (route, _) = sink.GetRouteToSource(
source,
signalType,
tieLine.DestinationPort,
null
);
return null;
if (route != null && route.Routes != null && route.Routes.Count > 0)
{
// Routes[0] is the hop nearest the source: its InputPort is the
// port on the first switching device that receives the signal from
// the source side. The TieLine whose DestinationPort matches that
// port is the exact tie that was traversed, giving us the precise
// source output port via SourcePort — regardless of how many output
// ports the source device has.
var firstHop = route.Routes[0];
var sourceTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
tl.DestinationPort.Key == firstHop.InputPort.Key &&
tl.DestinationPort.ParentDevice.Key == firstHop.InputPort.ParentDevice.Key);
if (sourceTieLine != null)
{
Debug.LogDebug(this,
"Found route from {source} to {sink} with {count} hops",
source.Key,
sink.Key,
route.Routes.Count
);
return sourceTieLine;
}
}
}
var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route =>
{
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine);
return route.OutputPort != null
&& route.InputPort != null
&& route.OutputPort?.Key == tieLine.SourcePort.Key
&& route.OutputPort?.ParentDevice.Key
== tieLine.SourcePort.ParentDevice.Key;
});
if (currentRoute == null)
{
Debug.LogMessage(
Serilog.Events.LogEventLevel.Debug,
"No route through midpoint {midpoint} for outputPort {outputPort}",
this,
midpoint.Key,
tieLine.SourcePort
);
return null;
}
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint);
nextTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
{
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key);
return tl.DestinationPort.Key == currentRoute.InputPort.Key
&& tl.DestinationPort.ParentDevice.Key
== currentRoute.InputPort.ParentDevice.Key;
});
if (nextTieLine != null)
{
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found next tieLine {tieLine}. Walking the chain", this, nextTieLine);
return GetRootTieLine(nextTieLine);
}
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root tieLine {tieLine}", this,nextTieLine);
return nextTieLine;
}
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLIne Source Device {sourceDeviceKey} is IRoutingSource: {isIRoutingSource}", this, tieLine.SourcePort.ParentDevice.Key, tieLine.SourcePort.ParentDevice is IRoutingSource);
//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);
}
Debug.LogDebug(this, "No route found to any source from {sink}", sink.Key);
return null;
}
catch (Exception ex)
{
Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex);
Debug.LogError(this, "Error getting root tieLine: {message}", ex.Message);
Debug.LogDebug(ex, "StackTrace: ");
return null;
}
return null;
}
}
}

View file

@ -10,73 +10,78 @@ using Serilog.Events;
namespace PepperDash.Essentials.Core.Web
{
/// <summary>
/// Represents a EssentialsWebApi
/// </summary>
public class EssentialsWebApi : EssentialsDevice
{
private readonly WebApiServer _server;
/// <summary>
/// Represents a EssentialsWebApi
/// </summary>
public class EssentialsWebApi : EssentialsDevice
{
private readonly WebApiServer _server;
///<example>
/// http(s)://{ipaddress}/cws/{basePath}
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
/// </example>
private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
: "/api";
///<example>
/// http(s)://{ipaddress}/cws/{basePath}
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
/// </example>
private readonly string _defaultBasePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
? string.Format("/app{0:00}/api", InitialParametersClass.ApplicationNumber)
: "/api";
private const int DebugTrace = 0;
private const int DebugInfo = 1;
private const int DebugVerbose = 2;
private const int DebugTrace = 0;
private const int DebugInfo = 1;
private const int DebugVerbose = 2;
/// <summary>
/// Gets or sets the BasePath
/// </summary>
public string BasePath { get; private set; }
/// <summary>
/// Gets or sets the BasePath
/// </summary>
public string BasePath { get; private set; }
/// <summary>
/// Tracks if CWS is registered
/// </summary>
public bool IsRegistered
{
get { return _server.IsRegistered; }
}
/// <summary>
/// Tracks if CWS is registered
/// </summary>
public bool IsRegistered
{
get { return _server.IsRegistered; }
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
public EssentialsWebApi(string key, string name)
: this(key, name, null)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
public EssentialsWebApi(string key, string name)
: this(key, name, null)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
/// <param name="config"></param>
public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config)
: base(key, name)
{
Key = key;
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
/// <param name="config"></param>
public EssentialsWebApi(string key, string name, EssentialsWebApiPropertiesConfig config)
: base(key, name)
{
Key = key;
if (config == null)
BasePath = _defaultBasePath;
else
BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath;
if (config == null)
BasePath = _defaultBasePath;
else
BasePath = string.IsNullOrEmpty(config.BasePath) ? _defaultBasePath : config.BasePath;
_server = new WebApiServer(Key, Name, BasePath);
_server = new WebApiServer(Key, Name, BasePath);
SetupRoutes();
}
SetupRoutes();
}
private void SetupRoutes()
{
private void SetupRoutes()
{
var routes = new List<HttpCwsRoute>
{
new HttpCwsRoute("login")
{
Name = "Root",
RouteHandler = new LoginRequestHandler()
},
new HttpCwsRoute("versions")
{
Name = "ReportVersions",
@ -177,6 +182,11 @@ namespace PepperDash.Essentials.Core.Web
Name = "Get Routing Ports for a device",
RouteHandler = new GetRoutingPortsHandler()
},
new HttpCwsRoute("routingDevicesAndTieLines")
{
Name = "Get Routing Devices and TieLines",
RouteHandler = new GetRoutingDevicesAndTieLinesHandler()
},
};
AddRoute(routes);
@ -211,78 +221,79 @@ namespace PepperDash.Essentials.Core.Web
/// </summary>
/// <inheritdoc />
public override void Initialize()
{
AddRoute(new HttpCwsRoute("apiPaths") {
{
AddRoute(new HttpCwsRoute("apiPaths")
{
Name = "GetPaths",
RouteHandler = new GetRoutesHandler(_server.GetRouteCollection(), BasePath)
});
// If running on an appliance
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance)
{
/*
{
/*
WEBSERVER [ON | OFF | TIMEOUT <VALUE IN SECONDS> | MAXSESSIONSPERUSER <Number of sessions>]
*/
var response = string.Empty;
CrestronConsole.SendControlSystemCommand("webserver", ref response);
if (response.Contains("OFF")) return;
var response = string.Empty;
CrestronConsole.SendControlSystemCommand("webserver", ref response);
if (response.Contains("OFF")) return;
var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
var is4Series = eCrestronSeries.Series4 == (Global.ProcessorSeries & eCrestronSeries.Series4);
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on {0} Appliance", is4Series ? "4-series" : "3-series");
_server.Start();
_server.Start();
GetPaths();
GetPaths();
return;
}
return;
}
// Automatically start CWS when running on a server (Linux OS, Virtual Control)
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
// Automatically start CWS when running on a server (Linux OS, Virtual Control)
Debug.LogMessage(LogEventLevel.Verbose, "Starting Essentials Web API on Virtual Control Server");
_server.Start();
_server.Start();
GetPaths();
}
GetPaths();
}
/// <summary>
/// Print the available pahts
/// </summary>
/// <example>
/// http(s)://{ipaddress}/cws/{basePath}
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
/// </example>
/// <summary>
/// GetPaths method
/// </summary>
public void GetPaths()
{
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
/// <summary>
/// Print the available paths
/// </summary>
/// <example>
/// http(s)://{ipaddress}/cws/{basePath}
/// http(s)://{ipaddress}/VirtualControl/Rooms/{roomId}/cws/{basePath}
/// </example>
/// <summary>
/// GetPaths method
/// </summary>
public void GetPaths()
{
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
var currentIp = CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
var hostname = CrestronEthernetHelper.GetEthernetParameter(
var currentIp = CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
var hostname = CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
var path = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server
? $"https://{hostname}/VirtualControl/Rooms/{InitialParametersClass.RoomId}/cws{BasePath}"
: $"https://{currentIp}/cws{BasePath}";
Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
var routeCollection = _server.GetRouteCollection();
if (routeCollection == null)
{
Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null");
return;
}
Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:");
foreach (var route in routeCollection)
{
Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
}
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
}
}
Debug.LogMessage(LogEventLevel.Information, this, "Server:{path:l}", path);
var routeCollection = _server.GetRouteCollection();
if (routeCollection == null)
{
Debug.LogMessage(LogEventLevel.Information, this, "Server route collection is null");
return;
}
Debug.LogMessage(LogEventLevel.Information, this, "Configured Routes:");
foreach (var route in routeCollection)
{
Debug.LogMessage(LogEventLevel.Information, this, "{routeName:l}: {routePath:l}/{routeUrl:l}", route.Name, path, route.Url);
}
Debug.LogMessage(LogEventLevel.Information, this, new string('-', 50));
}
}
}

View file

@ -36,17 +36,25 @@ namespace PepperDash.Essentials.Core.Web
};
}
/// <summary>
/// MapToDeviceListObject method
/// </summary>
public static object MapToDeviceListObject(IKeyed device)
{
var interfaces = device.GetType()
.GetInterfaces()
.Select(i => i.Name)
.ToList();
return new
{
device.Key,
Name = (device is IKeyName)
? (device as IKeyName).Name
: "---"
: "---",
Interfaces = interfaces
};
}
@ -110,5 +118,10 @@ namespace PepperDash.Essentials.Core.Web
CType = device.Value.Type == null ? "---": device.Value.Type.ToString()
};
}
}
internal static bool IsAuthenticated(HttpCwsContext context)
{
throw new NotImplementedException();
}
}
}

View file

@ -0,0 +1,178 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Core.Web.RequestHandlers;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
/// <summary>
/// Handles HTTP requests to retrieve routing devices and tielines information
/// </summary>
public class GetRoutingDevicesAndTieLinesHandler : WebApiBaseRequestHandler
{
public GetRoutingDevicesAndTieLinesHandler() : base(true) { }
protected override void HandleGet(HttpCwsContext context)
{
var devices = new List<RoutingDeviceInfo>();
// Get all devices from DeviceManager
foreach (var device in DeviceManager.AllDevices)
{
var deviceInfo = new RoutingDeviceInfo
{
Key = device.Key,
Name = (device as IKeyName)?.Name ?? device.Key
};
// Check if device implements IRoutingInputs
if (device is IRoutingInputs inputDevice)
{
deviceInfo.HasInputs = true;
deviceInfo.InputPorts = inputDevice.InputPorts.Select(p => new PortInfo
{
Key = p.Key,
SignalType = p.Type.ToString(),
ConnectionType = p.ConnectionType.ToString(),
IsInternal = p.IsInternal
}).ToList();
}
// Check if device implements IRoutingOutputs
if (device is IRoutingOutputs outputDevice)
{
deviceInfo.HasOutputs = true;
deviceInfo.OutputPorts = outputDevice.OutputPorts.Select(p => new PortInfo
{
Key = p.Key,
SignalType = p.Type.ToString(),
ConnectionType = p.ConnectionType.ToString(),
IsInternal = p.IsInternal
}).ToList();
}
// Check if device implements IRoutingInputsOutputs
if (device is IRoutingInputsOutputs)
{
deviceInfo.HasInputsAndOutputs = true;
}
// Only include devices that have routing capabilities
if (deviceInfo.HasInputs || deviceInfo.HasOutputs)
{
devices.Add(deviceInfo);
}
}
// Get all tielines
var tielines = TieLineCollection.Default.Select(tl => new TieLineInfo
{
SourceDeviceKey = tl.SourcePort.ParentDevice.Key,
SourcePortKey = tl.SourcePort.Key,
DestinationDeviceKey = tl.DestinationPort.ParentDevice.Key,
DestinationPortKey = tl.DestinationPort.Key,
SignalType = tl.Type.ToString(),
IsInternal = tl.IsInternal
}).ToList();
var response = new RoutingSystemInfo
{
Devices = devices,
TieLines = tielines
};
var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented);
context.Response.StatusCode = 200;
context.Response.StatusDescription = "OK";
context.Response.ContentType = "application/json";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(jsonResponse, false);
context.Response.End();
}
}
/// <summary>
/// Represents the complete routing system information including devices and tielines
/// </summary>
public class RoutingSystemInfo
{
[JsonProperty("devices")]
public List<RoutingDeviceInfo> Devices { get; set; }
[JsonProperty("tieLines")]
public List<TieLineInfo> TieLines { get; set; }
}
/// <summary>
/// Represents a routing device with its ports information
/// </summary>
public class RoutingDeviceInfo
{
[JsonProperty("key")]
public string Key { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("hasInputs")]
public bool HasInputs { get; set; }
[JsonProperty("hasOutputs")]
public bool HasOutputs { get; set; }
[JsonProperty("hasInputsAndOutputs")]
public bool HasInputsAndOutputs { get; set; }
[JsonProperty("inputPorts", NullValueHandling = NullValueHandling.Ignore)]
public List<PortInfo> InputPorts { get; set; }
[JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)]
public List<PortInfo> OutputPorts { get; set; }
}
/// <summary>
/// Represents a routing port with its properties
/// </summary>
public class PortInfo
{
[JsonProperty("key")]
public string Key { get; set; }
[JsonProperty("signalType")]
public string SignalType { get; set; }
[JsonProperty("connectionType")]
public string ConnectionType { get; set; }
[JsonProperty("isInternal")]
public bool IsInternal { get; set; }
}
/// <summary>
/// Represents a tieline connection between two ports
/// </summary>
public class TieLineInfo
{
[JsonProperty("sourceDeviceKey")]
public string SourceDeviceKey { get; set; }
[JsonProperty("sourcePortKey")]
public string SourcePortKey { get; set; }
[JsonProperty("destinationDeviceKey")]
public string DestinationDeviceKey { get; set; }
[JsonProperty("destinationPortKey")]
public string DestinationPortKey { get; set; }
[JsonProperty("signalType")]
public string SignalType { get; set; }
[JsonProperty("isInternal")]
public bool IsInternal { get; set; }
}
}

View file

@ -0,0 +1,122 @@
using System;
using Crestron.SimplSharp.CrestronAuthentication;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers;
namespace PepperDash.Essentials.Core.Web.RequestHandlers
{
/// <summary>
/// Represents a LoginRequestHandler
/// </summary>
public class LoginRequestHandler : WebApiBaseRequestHandler
{
/// <summary>
/// Constructor
/// </summary>
/// <remarks>
/// base(true) enables CORS support by default
/// </remarks>
public LoginRequestHandler()
: base(true)
{
}
/// <summary>
/// Handles POST method requests for user login and token generation
/// </summary>
/// <param name="context">The HTTP context for the request.</param>
protected override void HandlePost(HttpCwsContext context)
{
try
{
if (context.Request.ContentLength < 0)
{
context.Response.StatusCode = 400;
context.Response.StatusDescription = "Bad Request";
context.Response.End();
return;
}
var data = context.Request.GetRequestBody();
if (string.IsNullOrEmpty(data))
{
context.Response.StatusCode = 400;
context.Response.StatusDescription = "Bad Request";
context.Response.End();
return;
}
var loginRequest = JsonConvert.DeserializeObject<LoginRequest>(data);
if (loginRequest == null || string.IsNullOrEmpty(loginRequest.Username) || string.IsNullOrEmpty(loginRequest.Password))
{
context.Response.StatusCode = 400;
context.Response.StatusDescription = "Bad Request";
context.Response.End();
return;
}
Authentication.UserToken token;
try
{
token = Authentication.GetAuthenticationToken(loginRequest.Username, loginRequest.Password);
}
catch (ArgumentException)
{
context.Response.StatusCode = 401;
context.Response.StatusDescription = "Bad Request";
context.Response.End();
return;
}
if (!token.Valid)
{
context.Response.StatusCode = 401;
context.Response.StatusDescription = "Unauthorized";
context.Response.End();
return;
}
context.Response.StatusCode = 200;
context.Response.StatusDescription = "OK";
context.Response.ContentType = "application/json";
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
context.Response.Write(JsonConvert.SerializeObject(new { Token = token }, Formatting.Indented), false);
context.Response.End();
}
catch (System.Exception ex)
{
context.Response.StatusCode = 500;
context.Response.StatusDescription = "Internal Server Error";
context.Response.ContentType = "application/json";
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
context.Response.Write(JsonConvert.SerializeObject(new { Error = ex.Message }, Formatting.Indented), false);
context.Response.End();
}
}
}
/// <summary>
/// Represents a LoginRequest
/// </summary>
public class LoginRequest
{
/// <summary>
/// Gets or sets the username.
/// </summary>
public string Username { get; set; }
/// <summary>
/// Gets or sets the password.
/// </summary>
public string Password { get; set; }
}
}