mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-04-11 07:28:20 +00:00
Compare commits
1 Commits
v2.29.0-ti
...
copilot/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05feeddf37 |
@@ -2,35 +2,13 @@
|
||||
|
||||
## TL;DR
|
||||
|
||||
Routing is defined by a connection graph or a wiring diagram. Routable devices are sources, midpoints, or destinations. Devices are connected by tie lines. Tie lines represent the cables connecting devices, and have specific signal types (audio, video, audioVideo, secondaryAudio, usbInput, usbOutput). Routes are made by telling a destination to get a route from a source for a specific signal type. Combined signal types (e.g., audioVideo) are automatically split into separate routing operations.
|
||||
Routing is defined by a connection graph or a wiring diagram. Routeable devices are sources, midpoints, or destinations. Devices are connected by tie lines. Tie lines represent the cables connecting devices, and are of type audio, video or both. Routes are made by telling a destination to get an audio/video/combined route from a source.
|
||||
|
||||
## Summary
|
||||
|
||||
Essentials routing is described by defining a graph of connections between devices in a system, typically in configuration. The audio, video and combination connections are like a wiring diagram. This graph is a collection of devices and tie lines, each tie line connecting a source device, source output port, destination device and destination input port. Tie lines are logically represented as a collection.
|
||||
|
||||
When routes are to be executed, Essentials will use this connection graph to decide on routes from source to destination. A method call is made on a destination, which says "destination, find a way for source xyz to get to you." An algorithm analyzes the tie lines, instantly walking backwards from the destination, down every connection until it finds a complete path from the source. If a connected path is found, the algorithm then walks forward through all midpoints to the destination, executing switches as required until the full route is complete. The developer or configurer only needs to say "destination, get source xyz" and Essentials figures out how, regardless of what devices lie in between.
|
||||
|
||||
### Signal Type Handling
|
||||
|
||||
When a combined signal type like `audioVideo` is requested, Essentials automatically splits it into two separate routing operations—one for audio and one for video. Each signal type is routed independently through the system, ensuring that:
|
||||
- Audio-only tie lines can be used for the audio portion
|
||||
- Video-only tie lines can be used for the video portion
|
||||
- AudioVideo tie lines can be used for both portions
|
||||
|
||||
During path discovery, **only tie lines that support the requested signal type are considered**. For example, if a video route is requested, only tie lines with the video flag will be evaluated. This ensures signal compatibility throughout the entire routing chain.
|
||||
|
||||
### Port-Specific Routing
|
||||
|
||||
The routing system supports routing to and from specific ports on devices. You can specify:
|
||||
- A specific input port on the destination device
|
||||
- A specific output port on the source device
|
||||
- Both specific ports for precise routing control
|
||||
|
||||
When no specific ports are specified, the algorithm will automatically discover the appropriate ports based on available tie lines.
|
||||
|
||||
### Request Queuing
|
||||
|
||||
All routing requests are processed sequentially through a queue. For devices that implement warming/cooling behavior (e.g., projectors), route requests are automatically held when a device is cooling down and executed once the device is ready. This prevents routing errors and ensures proper device state management.
|
||||
When routes are to be executed, Essentials will use this connection graph to decide on routes from source to destination. A method call is made on a destination, which says “destination, find a way for source xyz to get to you.” An algorithm analyzes the tie lines, instantly walking backwards from the destination, down every connection until it finds a complete path from the source. If a connected path is found, the algorithm then walks forward through all midpoints to the destination, executing switches as required until the full route is complete. The developer or configurer only needs to say “destination, get source xyz” and Essentials figures out how, regardless of what devices lie in between.
|
||||
|
||||
### Classes Referenced
|
||||
|
||||
@@ -45,15 +23,15 @@ All routing requests are processed sequentially through a queue. For devices tha
|
||||
|
||||
The diagram below shows the connections in a simple presentation system, with a few variations in connection paths. Example routes will be described following the diagram.
|
||||
|
||||
Each visible line between ports on devices represents a tie line. A tie line connects an output port on one device to an input port on another device, for example: an HDMI port on a document camera to an HDMI input on a matrix switcher. A tie line has a signal type (audio, video, audioVideo, secondaryAudio, usbInput, or usbOutput) that determines what signals can travel through it. It is essentially a logical representation of a physical cable in a system. This diagram has 12 tie lines, and those tie lines are defined in the tieLines array in configuration.
|
||||
Each visible line between ports on devices represents a tie line. A tie line connects an output port on one device to an input port on another device, for example: an HDMI port on a document camera to an HDMI input on a matrix switcher. A tie line may be audio, video or both. It is essentially a logical representation of a physical cable in a system. This diagram has 12 tie lines, and those tie lines are defined in the tieLines array in configuration.
|
||||
|
||||

|
||||
|
||||
Let’s go through some examples of routing, using pseudo-code:
|
||||
|
||||
1. Method call: “Projector 1, show Doc cam.” Routing will walk backwards through DM-RMC-3 and DM-8x8 iterating through all “wired up” ports until it finds a path back to the Doc cam. Routing will then step back through all devices in the discovered chain, switching routes on those that are switchable: Doc cam: no switching; DM 8x8: route input 3 to output 3; DM-RMC-3: no switching; Projector 1: Select input HDMI In. Route is complete.
|
||||
2. Method call: "Projector 2, show Laptop, video-only." Routing will walk backwards through DM-RMC-4, DM 8x8, DM-TX-1, iterating through all connected ports until it finds a connection to the laptop. During this search, only tie lines that support video signals are considered. Routing then steps back through all devices, switching video where it can: Laptop: No switching; DM-TX-1: Select HDMI in; DM 8x8: Route input 5 to output 4; DM-RMC-4: No switching; Projector 2: Select HDMI input. Route is complete.
|
||||
3. Method call: "Amplifier, connect Laptop audio." Again walking backwards to Laptop, as in #2 above, but this time only tie lines supporting audio signals are evaluated. Switching will take place on DM-TX-1, DM 8x8, audio-only.
|
||||
2. Method call: “Projector 2, show Laptop, video-only.” Routing will walk backwards through DM-RMC-4, DM 8x8, DM-TX-1, iterating through all connected ports until it finds a connection to the laptop. Routing then steps back through all devices, switching video where it can: Laptop: No switching; DM-TX-1: Select HDMI in; DM 8x8: Route input 5 to output 4; DM-RMC-4: No switching; Projector 2: Select HDMI input. Route is complete.
|
||||
3. Method call: “Amplifier, connect Laptop audio.” Again walking backwards to Laptop, as in #2 above. Switching will take place on DM-TX-1, DM 8x8, audio-only.
|
||||
4. Very simple call: “Lobby display, show signage controller.” Routing will walk back on HDMI input 1 and immediately find the signage controller. It then does a switch to HDMI 1 on the display.
|
||||
|
||||
All four of the above could be logically combined in a series of calls to define a possible “scene” in a room: Put Document camera on Projector 1, put Laptop on Projector 2 and the audio, put Signage on the Lobby display. They key takeaway is that the developer doesn’t need to define what is involved in making a certain route. The person configuring the system defines how it’s wired up, and the code only needs to tell a given destination to get a source, likely through configuration as well.
|
||||
@@ -62,37 +40,6 @@ All of the above routes can be defined in source list routing tables, covered el
|
||||
|
||||
---
|
||||
|
||||
## Routing Algorithm Details
|
||||
|
||||
### Combined Signal Type Splitting
|
||||
|
||||
When an `audioVideo` route is requested, the routing system automatically splits it into two independent routing operations:
|
||||
|
||||
1. **Audio Route**: Finds the best path for audio signals from source to destination
|
||||
2. **Video Route**: Finds the best path for video signals from source to destination
|
||||
|
||||
Each route can take a different physical path through the system. For example:
|
||||
- Video might travel: Laptop → DM-TX-1 → DM Matrix → Display
|
||||
- Audio might travel: Laptop → DM-TX-1 → DM Matrix → Audio Processor → Amplifier
|
||||
|
||||
Both routes are discovered, stored, and executed independently. This allows for flexible system designs where audio and video follow different paths.
|
||||
|
||||
The same splitting behavior occurs for `Video + SecondaryAudio` requests, where video and secondary audio are routed as separate operations.
|
||||
|
||||
### Signal Type Filtering
|
||||
|
||||
At each step of the route discovery process, the algorithm filters tie lines based on the requested signal type:
|
||||
|
||||
- **Video request**: Only considers tie lines with the `video` flag set
|
||||
- **Audio request**: Only considers tie lines with the `audio` flag set
|
||||
- **AudioVideo request**: Routes audio and video separately, each following their respective filtering rules
|
||||
|
||||
If no tie line exists with the required signal type at any point in the chain, that path is rejected and the algorithm continues searching for an alternative route. If no valid path is found, the route request fails and no switching occurs.
|
||||
|
||||
This filtering ensures that incompatible signal types never interfere with routing decisions. For example, an audio-only cable will never be selected when routing video, preventing misconfiguration errors.
|
||||
|
||||
---
|
||||
|
||||
### Definitions
|
||||
|
||||
#### Ports
|
||||
@@ -117,101 +64,18 @@ A sink is a device at the end of a full signal path. For example, a display, amp
|
||||
|
||||
#### Tie-line
|
||||
|
||||
A tie-line is a logical representation of a physical cable connection between two devices. It has five properties that define how the tie-line connects two devices.
|
||||
|
||||
##### How Tie Line Types Are Determined
|
||||
|
||||
The effective type of a tie line is determined by one of two methods:
|
||||
|
||||
1. **Automatic (Recommended)**: When no `type` property is specified in configuration, the tie line's type is automatically calculated as the **intersection** of signal types supported by both the source and destination ports. This ensures only compatible signals are considered for routing.
|
||||
|
||||
Example: If a source port supports `AudioVideo` and the destination port supports `Audio`, the tie line will have type `Audio` (the only common type).
|
||||
|
||||
2. **Manual Override**: When the `type` property is explicitly set, it overrides the automatic calculation. This is useful when the physical cable supports fewer signal types than both ports are capable of.
|
||||
|
||||
Example: Both ports support `AudioVideo`, but the cable only carries audio, so you set `"type": "audio"`.
|
||||
|
||||
##### Validation
|
||||
|
||||
At startup, tie line configurations are validated to ensure:
|
||||
- Both ports exist on their respective devices
|
||||
- The source and destination ports have at least one common signal type
|
||||
- If a `type` override is specified, both ports must support that signal type
|
||||
|
||||
Invalid tie lines will fail to build with descriptive error messages, preventing runtime routing issues.
|
||||
|
||||
##### Signal Types
|
||||
|
||||
Tie lines support the following signal types:
|
||||
|
||||
- `audio` - Audio-only signals
|
||||
- `video` - Video-only signals
|
||||
- `audioVideo` - Combined audio and video (automatically split during routing)
|
||||
- `secondaryAudio` - Secondary audio channel (e.g., program audio separate from microphone audio)
|
||||
- `usbInput` - USB input signals
|
||||
- `usbOutput` - USB output signals
|
||||
|
||||
The `type` property determines which signals can travel through the tie line. During route discovery, only tie lines matching the requested signal type will be considered as valid paths.
|
||||
|
||||
**Note**: In most cases, you should omit the `type` property and let the system automatically calculate it from the port capabilities. Only use it when you need to restrict the tie line to fewer signal types than the ports support or when needed for clarity.
|
||||
|
||||
##### Configuration Examples
|
||||
|
||||
**Example 1: Automatic type calculation (recommended)**
|
||||
|
||||
Connecting an HDMI cable between devices that both support audio and video. The `type` property is omitted, so the tie line will automatically support `AudioVideo`:
|
||||
A tie-line is a logical representation of a physical cable connection between two devices. It has five properties that define how the tie-line connects two devices. A configuration snippet for a single tie line connecting HDMI output 1 on a Cisco RoomKit to HDMI input 1 on a display, carrying both audio and video, is shown below.
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceKey": "ciscoSparkPlusCodec-1",
|
||||
"sourcePort": "HdmiOut1",
|
||||
"destinationKey": "display-1",
|
||||
"destinationPort": "HdmiIn1"
|
||||
"destinationPort": "HdmiIn1",
|
||||
"type": "audioVideo"
|
||||
}
|
||||
```
|
||||
|
||||
**Example 2: Type override for cable limitations**
|
||||
|
||||
Both devices support `AudioVideo`, but the physical cable only carries audio. The `type` property restricts routing to audio only:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceKey": "dmSwitcher-1",
|
||||
"sourcePort": "audioVideoOut1",
|
||||
"destinationKey": "amplifier-1",
|
||||
"destinationPort": "audioVideoIn1",
|
||||
"type": "audio"
|
||||
}
|
||||
```
|
||||
|
||||
**Example 3: Mismatched port types (automatically handled)**
|
||||
|
||||
Source only supports audio, destination supports both. No `type` needed—the tie line will automatically be `Audio`:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceKey": "audioProcessor-1",
|
||||
"sourcePort": "audioOut1",
|
||||
"destinationKey": "dmSwitcher-1",
|
||||
"destinationPort": "audioVideoIn1"
|
||||
}
|
||||
```
|
||||
|
||||
**Invalid Example: Incompatible types**
|
||||
|
||||
This configuration will **fail validation** at startup because the ports have no common signal types:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceKey": "audioProcessor-1",
|
||||
"sourcePort": "audioOut1",
|
||||
"destinationKey": "display-1",
|
||||
"destinationPort": "hdmiIn1",
|
||||
"type": "video"
|
||||
}
|
||||
```
|
||||
Error: `"Override type 'Video' is not supported by source port 'audioOut1' (type: Audio)"`
|
||||
|
||||
### Interfaces
|
||||
|
||||
Todo: Define Interfaces IRouting, IRoutingOutputs, IRoutingInputs
|
||||
|
||||
@@ -30,17 +30,6 @@ namespace PepperDash.Essentials.Core.Bridges
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="name"></param>
|
||||
protected BridgeApi(string key, string name) :
|
||||
base(key, name)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -69,7 +58,7 @@ namespace PepperDash.Essentials.Core.Bridges
|
||||
/// <param name="dc">Device configuration</param>
|
||||
/// <param name="eisc">EISC instance</param>
|
||||
public EiscApiAdvanced(DeviceConfig dc, BasicTriList eisc) :
|
||||
base(dc.Key, dc.Name)
|
||||
base(dc.Key)
|
||||
{
|
||||
JoinMaps = new Dictionary<string, JoinMapBaseAdvanced>();
|
||||
|
||||
|
||||
@@ -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("key: {0} signalType: {1}{2}", routingInputPort.Key, routingInputPort.Type, CrestronEnvironment.NewLine);
|
||||
CrestronConsole.ConsoleCommandResponse("{0}{1}", routingInputPort.Key, 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("key: {0} signalType: {1}{2}", routingOutputPort.Key, routingOutputPort.Type, CrestronEnvironment.NewLine);
|
||||
CrestronConsole.ConsoleCommandResponse("{0}{1}", routingOutputPort.Key, CrestronEnvironment.NewLine);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,17 +36,8 @@ namespace PepperDash.Essentials.Core
|
||||
{
|
||||
var programAssemblies = Directory.GetFiles(InitialParametersClass.ProgramDirectory.ToString(), "*.dll");
|
||||
|
||||
// Assemblies known to cause load errors that should be skipped
|
||||
var assembliesToSkip = new[] { "CrestronOnvif.dll" };
|
||||
|
||||
foreach (var assembly in programAssemblies)
|
||||
{
|
||||
if (assembliesToSkip.Any(a => Path.GetFileName(assembly).Equals(a, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Skipping assembly: {assemblyName}", Path.GetFileName(assembly));
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Assembly.LoadFrom(assembly);
|
||||
|
||||
@@ -18,20 +18,6 @@ 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.
|
||||
@@ -43,105 +29,6 @@ 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"
|
||||
/// </summary>
|
||||
private static readonly HashSet<string> _impossibleRoutes = new HashSet<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Indexes all TieLines by source and destination device keys for faster lookups.
|
||||
/// Should be called once at system startup after all TieLines are created.
|
||||
/// </summary>
|
||||
public static void IndexTieLines()
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, "Indexing TieLines for faster route discovery");
|
||||
|
||||
_tieLinesByDestination = TieLineCollection.Default
|
||||
.GroupBy(t => t.DestinationPort.ParentDevice.Key)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
_tieLinesBySource = TieLineCollection.Default
|
||||
.GroupBy(t => t.SourcePort.ParentDevice.Key)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Information, "TieLine indexing complete. {0} destination keys, {1} source keys",
|
||||
null, _tieLinesByDestination.Count, _tieLinesBySource.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogError("Exception indexing TieLines: {exception}", ex.Message);
|
||||
Debug.LogDebug(ex, "Stack Trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets TieLines connected to a destination device.
|
||||
/// Uses indexed lookup if available, otherwise falls back to LINQ query.
|
||||
/// </summary>
|
||||
/// <param name="destinationKey">The destination device key</param>
|
||||
/// <returns>List of TieLines connected to the destination</returns>
|
||||
private static IEnumerable<TieLine> GetTieLinesForDestination(string destinationKey)
|
||||
{
|
||||
if (_tieLinesByDestination != null && _tieLinesByDestination.TryGetValue(destinationKey, out List<TieLine> tieLines))
|
||||
{
|
||||
return tieLines;
|
||||
}
|
||||
|
||||
// Fallback to LINQ if index not available
|
||||
return TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destinationKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets TieLines connected to a source device.
|
||||
/// Uses indexed lookup if available, otherwise falls back to LINQ query.
|
||||
/// </summary>
|
||||
/// <param name="sourceKey">The source device key</param>
|
||||
/// <returns>List of TieLines connected to the source</returns>
|
||||
private static IEnumerable<TieLine> GetTieLinesForSource(string sourceKey)
|
||||
{
|
||||
if (_tieLinesBySource != null && _tieLinesBySource.TryGetValue(sourceKey, out List<TieLine> tieLines))
|
||||
{
|
||||
return tieLines;
|
||||
}
|
||||
|
||||
// Fallback to LINQ if index not available
|
||||
return TieLineCollection.Default.Where(t => t.SourcePort.ParentDevice.Key == sourceKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a cache key for route impossibility tracking.
|
||||
/// </summary>
|
||||
/// <param name="sourceKey">Source device key</param>
|
||||
/// <param name="destKey">Destination device key</param>
|
||||
/// <param name="type">Signal type</param>
|
||||
/// <returns>Cache key string</returns>
|
||||
private static string GetRouteKey(string sourceKey, string destKey, eRoutingSignalType type)
|
||||
{
|
||||
return string.Format("{0}|{1}|{2}", sourceKey, destKey, type);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime.
|
||||
/// </summary>
|
||||
public static void ClearImpossibleRoutesCache()
|
||||
{
|
||||
_impossibleRoutes.Clear();
|
||||
Debug.LogMessage(LogEventLevel.Information, "Impossible routes cache cleared");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute
|
||||
/// and then attempts a new Route and if sucessful, stores that RouteDescriptor
|
||||
@@ -286,9 +173,8 @@ namespace PepperDash.Essentials.Core
|
||||
if (!audioSuccess && !videoSuccess)
|
||||
return (null, null);
|
||||
|
||||
// Return null for descriptors that have no routes
|
||||
return (audioSuccess && audioRouteDescriptor.Routes.Count > 0 ? audioRouteDescriptor : null,
|
||||
videoSuccess && videoRouteDescriptor.Routes.Count > 0 ? videoRouteDescriptor : null);
|
||||
|
||||
return (audioRouteDescriptor, videoRouteDescriptor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -359,90 +245,6 @@ 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.
|
||||
@@ -455,51 +257,7 @@ namespace PepperDash.Essentials.Core
|
||||
if (request.Source == null)
|
||||
return;
|
||||
|
||||
RouteDescriptor audioOrSingleRoute = null;
|
||||
RouteDescriptor videoRoute = null;
|
||||
|
||||
// Try to use pre-loaded route descriptors first
|
||||
if (request.SignalType.HasFlag(eRoutingSignalType.AudioVideo))
|
||||
{
|
||||
// For AudioVideo routes, check both Audio and Video collections
|
||||
if (RouteDescriptors.TryGetValue(eRoutingSignalType.Audio, out RouteDescriptorCollection audioCollection))
|
||||
{
|
||||
audioOrSingleRoute = audioCollection.Descriptors.FirstOrDefault(d =>
|
||||
d.Source.Key == request.Source.Key &&
|
||||
d.Destination.Key == request.Destination.Key &&
|
||||
(request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key));
|
||||
}
|
||||
|
||||
if (RouteDescriptors.TryGetValue(eRoutingSignalType.Video, out RouteDescriptorCollection videoCollection))
|
||||
{
|
||||
videoRoute = videoCollection.Descriptors.FirstOrDefault(d =>
|
||||
d.Source.Key == request.Source.Key &&
|
||||
d.Destination.Key == request.Destination.Key &&
|
||||
(request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For single signal type routes
|
||||
var signalTypeToCheck = request.SignalType.HasFlag(eRoutingSignalType.SecondaryAudio)
|
||||
? eRoutingSignalType.SecondaryAudio
|
||||
: request.SignalType;
|
||||
|
||||
if (RouteDescriptors.TryGetValue(signalTypeToCheck, out RouteDescriptorCollection collection))
|
||||
{
|
||||
audioOrSingleRoute = collection.Descriptors.FirstOrDefault(d =>
|
||||
d.Source.Key == request.Source.Key &&
|
||||
d.Destination.Key == request.Destination.Key &&
|
||||
(request.DestinationPort == null || d.InputPort?.Key == request.DestinationPort.Key));
|
||||
}
|
||||
}
|
||||
|
||||
// If no pre-loaded route found, build it dynamically
|
||||
if (audioOrSingleRoute == null && videoRoute == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Debug, "No pre-loaded route found, building dynamically", request.Destination);
|
||||
(audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort);
|
||||
}
|
||||
var (audioOrSingleRoute, videoRoute) = request.Destination.GetRouteToSource(request.Source, request.SignalType, request.DestinationPort, request.SourcePort);
|
||||
|
||||
if (audioOrSingleRoute == null && videoRoute == null)
|
||||
return;
|
||||
@@ -563,13 +321,13 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
/// <param name="destination"></param>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="outputPortToUse">The RoutingOutputPort whose link is being checked for a route</param>
|
||||
/// <param name="destinationPort">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="destinationPort">The RoutingOutputPort whose link is being checked for a route</param>
|
||||
/// <param name="sourcePort">The source output port (optional)</param>
|
||||
/// <param name="outputPortToUse">The RoutingOutputPort to use for the route</param>
|
||||
/// <param name="sourcePort">The specific source output port to use (optional)</param>
|
||||
/// <returns>true if source is hit</returns>
|
||||
private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
|
||||
RoutingOutputPort outputPortToUse, List<IRoutingInputsOutputs> alreadyCheckedDevices,
|
||||
@@ -577,54 +335,42 @@ namespace PepperDash.Essentials.Core
|
||||
{
|
||||
cycle++;
|
||||
|
||||
// Check if this route has already been determined to be impossible
|
||||
var routeKey = GetRouteKey(source.Key, destination.Key, signalType);
|
||||
if (_impossibleRoutes.Contains(routeKey))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Route {0} is cached as impossible, skipping", null, routeKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString());
|
||||
|
||||
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 = allDestinationTieLines.Where(t =>
|
||||
t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo);
|
||||
destinationTieLines = TieLineCollection.Default.Where(t =>
|
||||
t.DestinationPort.ParentDevice.Key == destination.Key && (t.Type.HasFlag(signalType) || signalType == eRoutingSignalType.AudioVideo));
|
||||
}
|
||||
else
|
||||
{
|
||||
destinationTieLines = allDestinationTieLines.Where(t =>
|
||||
t.DestinationPort.Key == destinationPort.Key && t.Type.HasFlag(signalType));
|
||||
destinationTieLines = TieLineCollection.Default.Where(t => t.DestinationPort.ParentDevice.Key == destination.Key && 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.SourcePort.ParentDevice.Key == source.Key);
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && 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.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key);
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && 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.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key);
|
||||
directTie = destinationTieLines.FirstOrDefault(t => t.DestinationPort.ParentDevice.Key == destination.Key && 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.Key == destinationPort.Key && t.SourcePort.ParentDevice.Key == source.Key && t.SourcePort.Key == sourcePort.Key);
|
||||
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);
|
||||
}
|
||||
|
||||
if (directTie != null) // Found a tie directly to the source
|
||||
@@ -679,10 +425,6 @@ namespace PepperDash.Essentials.Core
|
||||
if (goodInputPort == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "No route found to {0}", destination, source.Key);
|
||||
|
||||
// Cache this as an impossible route
|
||||
_impossibleRoutes.Add(routeKey);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,15 +95,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
|
||||
{
|
||||
@@ -137,11 +137,98 @@ 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 $"Route table from {Source.Key} to {Destination.Key} for {SignalType}:\r\n {string.Join("\r\n ", routesText)}";
|
||||
return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText));
|
||||
}
|
||||
}
|
||||
|
||||
/*/// <summary>
|
||||
/// Represents an collection of individual route steps between Source and Destination
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Represents a RouteDescriptor
|
||||
/// </summary>
|
||||
public class RouteDescriptor<TInputSelector, TOutputSelector>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Destination
|
||||
/// </summary>
|
||||
public IRoutingInputs<TInputSelector> Destination { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the Source
|
||||
/// </summary>
|
||||
public IRoutingOutputs<TOutputSelector> Source { get; private set; }
|
||||
/// <summary>
|
||||
/// Gets or sets the SignalType
|
||||
/// </summary>
|
||||
public eRoutingSignalType SignalType { get; private set; }
|
||||
public List<RouteSwitchDescriptor<TInputSelector, TOutputSelector>> Routes { get; private set; }
|
||||
|
||||
|
||||
public RouteDescriptor(IRoutingOutputs<TOutputSelector> source, IRoutingInputs<TInputSelector> destination, eRoutingSignalType signalType)
|
||||
{
|
||||
Destination = destination;
|
||||
Source = source;
|
||||
SignalType = signalType;
|
||||
Routes = new List<RouteSwitchDescriptor<TInputSelector, TOutputSelector>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ExecuteRoutes method
|
||||
/// </summary>
|
||||
public void ExecuteRoutes()
|
||||
{
|
||||
foreach (var route in Routes)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "ExecuteRoutes: {0}", null, route.ToString());
|
||||
|
||||
if (route.SwitchingDevice is IRoutingSinkWithSwitching<TInputSelector> sink)
|
||||
{
|
||||
sink.ExecuteSwitch(route.InputPort.Selector);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (route.SwitchingDevice is IRouting switchingDevice)
|
||||
{
|
||||
switchingDevice.ExecuteSwitch(route.InputPort.Selector, route.OutputPort.Selector, SignalType);
|
||||
|
||||
route.OutputPort.InUseTracker.AddUser(Destination, "destination-" + SignalType);
|
||||
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Output port {0} routing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ReleaseRoutes method
|
||||
/// </summary>
|
||||
public void ReleaseRoutes()
|
||||
{
|
||||
foreach (var route in Routes)
|
||||
{
|
||||
if (route.SwitchingDevice is IRouting<TInputSelector, TOutputSelector>)
|
||||
{
|
||||
// Pull the route from the port. Whatever is watching the output's in use tracker is
|
||||
// responsible for responding appropriately.
|
||||
route.OutputPort.InUseTracker.RemoveUser(Destination, "destination-" + SignalType);
|
||||
Debug.LogMessage(LogEventLevel.Verbose, "Port {0} releasing. Count={1}", null, route.OutputPort.Key, route.OutputPort.InUseTracker.InUseCountFeedback.UShortValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ToString method
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
var routesText = Routes.Select(r => r.ToString()).ToArray();
|
||||
return string.Format("Route table from {0} to {1}:\r{2}", Source.Key, Destination.Key, string.Join("\r", routesText));
|
||||
}
|
||||
}*/
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Core;
|
||||
using Serilog.Events;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
@@ -12,7 +12,7 @@ namespace PepperDash.Essentials.Core
|
||||
public class RouteDescriptorCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the default collection of RouteDescriptors.
|
||||
/// DefaultCollection static property
|
||||
/// </summary>
|
||||
public static RouteDescriptorCollection DefaultCollection
|
||||
{
|
||||
@@ -27,11 +27,6 @@ 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
|
||||
@@ -45,29 +40,13 @@ namespace PepperDash.Essentials.Core
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a route already exists with the same source, destination, input port, AND signal type
|
||||
var existingRoute = RouteDescriptors.FirstOrDefault(t =>
|
||||
t.Source == descriptor.Source &&
|
||||
t.Destination == descriptor.Destination &&
|
||||
t.SignalType == descriptor.SignalType &&
|
||||
((t.InputPort == null && descriptor.InputPort == null) ||
|
||||
(t.InputPort != null && descriptor.InputPort != null && t.InputPort.Key == descriptor.InputPort.Key)));
|
||||
|
||||
if (existingRoute != null)
|
||||
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))
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, descriptor.Destination,
|
||||
"Route from {0} to {1}:{2} ({3}) already exists in this collection",
|
||||
descriptor?.Source?.Key,
|
||||
descriptor?.Destination?.Key,
|
||||
descriptor?.InputPort?.Key ?? "auto",
|
||||
descriptor?.SignalType);
|
||||
Debug.LogMessage(LogEventLevel.Debug, descriptor.Destination,
|
||||
"Route to [{0}] already exists in global routes table", descriptor?.Source?.Key);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -82,11 +61,11 @@ namespace PepperDash.Essentials.Core
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the route descriptor for a specific destination and input port
|
||||
/// Gets the RouteDescriptor for a destination and input port key. Returns null if no matching RouteDescriptor exists.
|
||||
/// </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>
|
||||
/// <param name="destination"></param>
|
||||
/// <param name="inputPortKey"></param>
|
||||
/// <returns></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);
|
||||
@@ -103,7 +82,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)
|
||||
@@ -114,4 +93,70 @@ 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;
|
||||
}
|
||||
}*/
|
||||
}
|
||||
@@ -4,51 +4,96 @@
|
||||
/// 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 SwitchingDevice { get { return InputPort?.ParentDevice; } }
|
||||
public IRoutingInputs<TInputSelector> SwitchingDevice { get { return InputPort.ParentDevice; } }
|
||||
/// <summary>
|
||||
/// The output port being switched from (relevant for matrix switchers). Null for sink devices.
|
||||
/// Gets or sets the OutputPort
|
||||
/// </summary>
|
||||
public RoutingOutputPort OutputPort { get; set; }
|
||||
public RoutingOutputPort<TOutputSelector> OutputPort { get; set; }
|
||||
/// <summary>
|
||||
/// The input port being switched to.
|
||||
/// Gets or sets the InputPort
|
||||
/// </summary>
|
||||
public RoutingInputPort InputPort { get; set; }
|
||||
public RoutingInputPort<TInputSelector> 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)
|
||||
public RouteSwitchDescriptor(RoutingInputPort<TInputSelector> 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)
|
||||
public RouteSwitchDescriptor(RoutingOutputPort<TOutputSelector> outputPort, RoutingInputPort<TInputSelector> inputPort)
|
||||
{
|
||||
InputPort = inputPort;
|
||||
OutputPort = outputPort;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representation of the route switch descriptor.
|
||||
/// ToString method
|
||||
/// </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")}";
|
||||
return string.Format("{0} switches output '{1}' to input '{2}'", SwitchingDevice.Key, OutputPort.Selector, InputPort.Selector);
|
||||
else
|
||||
return $"{(SwitchingDevice != null ? SwitchingDevice.Key : "No Device")} switches to input {(InputPort != null ? InputPort.Key : "No input port")}";
|
||||
return string.Format("{0} switches to input '{1}'", SwitchingDevice.Key, InputPort.Selector);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Crestron.SimplSharp;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Essentials.Core.Config;
|
||||
|
||||
@@ -13,21 +11,6 @@ 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, CTimer> updateTimers = new Dictionary<string, CTimer>();
|
||||
|
||||
/// <summary>
|
||||
/// Debounce delay in milliseconds
|
||||
/// </summary>
|
||||
private const long DEBOUNCE_MS = 500;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RoutingFeedbackManager"/> class.
|
||||
/// </summary>
|
||||
@@ -36,100 +19,10 @@ 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>();
|
||||
var midpoints = DeviceManager.AllDevices.OfType<IRoutingWithFeedback>();
|
||||
|
||||
foreach (var sink in sinks)
|
||||
{
|
||||
if (sink.CurrentInputPort == null)
|
||||
continue;
|
||||
|
||||
// Find all upstream midpoints for this sink
|
||||
var upstreamMidpoints = GetUpstreamMidpoints(sink);
|
||||
|
||||
foreach (var midpointKey in upstreamMidpoints)
|
||||
{
|
||||
if (!midpointToSinksMap.ContainsKey(midpointKey))
|
||||
midpointToSinksMap[midpointKey] = new HashSet<string>();
|
||||
|
||||
midpointToSinksMap[midpointKey].Add(sink.Key);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Information,
|
||||
"Built midpoint-to-sink map with {count} midpoints",
|
||||
this,
|
||||
midpointToSinksMap.Count
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all upstream midpoint device keys for a given sink
|
||||
/// </summary>
|
||||
private HashSet<string> GetUpstreamMidpoints(IRoutingSinkWithSwitchingWithInputPort sink)
|
||||
{
|
||||
var result = new HashSet<string>();
|
||||
var visited = new HashSet<string>();
|
||||
|
||||
if (sink.CurrentInputPort == null)
|
||||
return result;
|
||||
|
||||
var tieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
||||
tl.DestinationPort.Key == sink.CurrentInputPort.Key &&
|
||||
tl.DestinationPort.ParentDevice.Key == sink.CurrentInputPort.ParentDevice.Key);
|
||||
|
||||
if (tieLine == null)
|
||||
return result;
|
||||
|
||||
TraceUpstreamMidpoints(tieLine, result, visited);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively traces upstream to find all midpoint devices
|
||||
/// </summary>
|
||||
private void TraceUpstreamMidpoints(TieLine tieLine, HashSet<string> midpoints, HashSet<string> visited)
|
||||
{
|
||||
if (tieLine == null || visited.Contains(tieLine.SourcePort.ParentDevice.Key))
|
||||
return;
|
||||
|
||||
visited.Add(tieLine.SourcePort.ParentDevice.Key);
|
||||
|
||||
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
|
||||
{
|
||||
midpoints.Add(midpoint.Key);
|
||||
|
||||
// Find upstream TieLines connected to this midpoint's inputs
|
||||
var midpointInputs = (midpoint as IRoutingInputs)?.InputPorts;
|
||||
if (midpointInputs != null)
|
||||
{
|
||||
foreach (var inputPort in midpointInputs)
|
||||
{
|
||||
var upstreamTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
||||
tl.DestinationPort.Key == inputPort.Key &&
|
||||
tl.DestinationPort.ParentDevice.Key == inputPort.ParentDevice.Key);
|
||||
|
||||
if (upstreamTieLine != null)
|
||||
TraceUpstreamMidpoints(upstreamTieLine, midpoints, visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to the RouteChanged event on all devices implementing <see cref="IRoutingWithFeedback"/>.
|
||||
/// </summary>
|
||||
@@ -159,7 +52,7 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
|
||||
/// <summary>
|
||||
/// Handles the RouteChanged event from a midpoint device.
|
||||
/// Only triggers updates for sink devices that are downstream of this midpoint.
|
||||
/// 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>
|
||||
@@ -170,33 +63,12 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
{
|
||||
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
|
||||
);
|
||||
var devices =
|
||||
DeviceManager.AllDevices.OfType<IRoutingSinkWithSwitchingWithInputPort>();
|
||||
|
||||
foreach (var sinkKey in affectedSinkKeys)
|
||||
{
|
||||
if (DeviceManager.GetDeviceForKey(sinkKey) is IRoutingSinkWithSwitchingWithInputPort sink)
|
||||
{
|
||||
UpdateDestination(sink, sink.CurrentInputPort);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
foreach (var device in devices)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"Midpoint {midpoint} changed but has no downstream sinks in map",
|
||||
this,
|
||||
midpoint.Key
|
||||
);
|
||||
UpdateDestination(device, device.CurrentInputPort);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -241,7 +113,6 @@ 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>
|
||||
@@ -249,55 +120,6 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
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
|
||||
updateTimers[key] = new CTimer(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
UpdateDestinationImmediate(destination, inputPort);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
ex,
|
||||
"Error in debounced update for destination {destinationKey}: {message}",
|
||||
this,
|
||||
destination.Key,
|
||||
ex.Message
|
||||
);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (updateTimers.ContainsKey(key))
|
||||
{
|
||||
updateTimers[key]?.Dispose();
|
||||
updateTimers.Remove(key);
|
||||
}
|
||||
}
|
||||
}, null, DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately updates the CurrentSourceInfo for a destination device.
|
||||
/// Called after debounce delay.
|
||||
/// </summary>
|
||||
private void UpdateDestinationImmediate(
|
||||
IRoutingSinkWithSwitching destination,
|
||||
RoutingInputPort inputPort
|
||||
)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
@@ -487,98 +309,105 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// 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
|
||||
{
|
||||
if (!(tieLine.DestinationPort.ParentDevice is IRoutingInputs sink))
|
||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "**Following tieLine {tieLine}**", this, tieLine);
|
||||
|
||||
if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"TieLine destination {device} is not IRoutingInputs",
|
||||
this,
|
||||
tieLine.DestinationPort.ParentDevice.Key
|
||||
);
|
||||
return null;
|
||||
}
|
||||
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "TieLine Source device {sourceDevice} is midpoint", this, midpoint);
|
||||
|
||||
// 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)
|
||||
if (midpoint.CurrentRoutes == null || midpoint.CurrentRoutes.Count == 0)
|
||||
{
|
||||
// Use the optimized route discovery with loop protection
|
||||
var (route, _) = sink.GetRouteToSource(
|
||||
source,
|
||||
signalType,
|
||||
tieLine.DestinationPort,
|
||||
null
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"Midpoint {midpointKey} has no routes",
|
||||
this,
|
||||
midpoint.Key
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var currentRoute = midpoint.CurrentRoutes.FirstOrDefault(route =>
|
||||
{
|
||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", this, route, tieLine);
|
||||
|
||||
return route.OutputPort != null
|
||||
&& route.InputPort != null
|
||||
&& route.OutputPort?.Key == tieLine.SourcePort.Key
|
||||
&& route.OutputPort?.ParentDevice.Key
|
||||
== tieLine.SourcePort.ParentDevice.Key;
|
||||
});
|
||||
|
||||
if (currentRoute == null)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"No route through midpoint {midpoint} for outputPort {outputPort}",
|
||||
this,
|
||||
midpoint.Key,
|
||||
tieLine.SourcePort
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found currentRoute {currentRoute} through {midpoint}", this, currentRoute, midpoint);
|
||||
|
||||
nextTieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
||||
{
|
||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Checking {route} against {tieLine}", tl.DestinationPort.Key, currentRoute.InputPort.Key);
|
||||
return tl.DestinationPort.Key == currentRoute.InputPort.Key
|
||||
&& tl.DestinationPort.ParentDevice.Key
|
||||
== currentRoute.InputPort.ParentDevice.Key;
|
||||
});
|
||||
|
||||
if (nextTieLine != null)
|
||||
{
|
||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found next tieLine {tieLine}. Walking the chain", this, nextTieLine);
|
||||
return GetRootTieLine(nextTieLine);
|
||||
}
|
||||
|
||||
//Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Found root tieLine {tieLine}", this,nextTieLine);
|
||||
return nextTieLine;
|
||||
}
|
||||
|
||||
Debug.LogMessage(
|
||||
Serilog.Events.LogEventLevel.Debug,
|
||||
"No route found to any source from {sink}",
|
||||
this,
|
||||
sink.Key
|
||||
//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
|
||||
);
|
||||
return null;
|
||||
|
||||
if (nextTieLine != null)
|
||||
{
|
||||
return GetRootTieLine(nextTieLine);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.LogMessage(
|
||||
ex,
|
||||
"Error getting root tieLine: {Exception}",
|
||||
this,
|
||||
ex
|
||||
);
|
||||
Debug.LogMessage(ex, "Error walking tieLines: {Exception}", this, ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PepperDash.Essentials.Core
|
||||
{
|
||||
@@ -21,24 +21,24 @@ namespace PepperDash.Essentials.Core
|
||||
//public int InUseCount { get { return DestinationUsingThis.Count; } }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of this tie line. Returns the intersection of signal types supported by both
|
||||
/// the source and destination ports (what signals can actually travel through this tie line),
|
||||
/// or the OverrideType when it is set.
|
||||
/// Gets the type of this tie line. Will either be the type of the destination port
|
||||
/// or the type of OverrideType when it is set.
|
||||
/// </summary>
|
||||
public eRoutingSignalType Type
|
||||
{
|
||||
get
|
||||
{
|
||||
if (OverrideType.HasValue) return OverrideType.Value;
|
||||
return SourcePort.Type & DestinationPort.Type;
|
||||
return DestinationPort.Type;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use this to override the Type property. For example, when both ports support AudioVideo
|
||||
/// but the physical cable only carries Audio or Video, setting this will limit the signal
|
||||
/// paths available to the routing algorithm without affecting the actual port types.
|
||||
/// When set, this value is used instead of the calculated intersection of source and destination types.
|
||||
/// Use this to override the Type property for the destination port. For example,
|
||||
/// when the tie line is type AudioVideo, and the signal flow should be limited to
|
||||
/// Audio-only or Video only, changing this type will alter the signal paths
|
||||
/// available to the routing algorithm without affecting the actual Type
|
||||
/// of the destination port.
|
||||
/// </summary>
|
||||
public eRoutingSignalType? OverrideType { get; set; }
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
/// <param name="sourcePort">The source output port.</param>
|
||||
/// <param name="destinationPort">The destination input port.</param>
|
||||
/// <param name="overrideType">The signal type to limit the link to. Overrides the calculated intersection of port types for routing calculations.</param>
|
||||
/// <param name="overrideType">The signal type to limit the link to. Overrides DestinationPort.Type for routing calculations.</param>
|
||||
public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType? overrideType) :
|
||||
this(sourcePort, destinationPort)
|
||||
{
|
||||
@@ -91,7 +91,7 @@ namespace PepperDash.Essentials.Core
|
||||
/// </summary>
|
||||
/// <param name="sourcePort">The source output port.</param>
|
||||
/// <param name="destinationPort">The destination input port.</param>
|
||||
/// <param name="overrideType">The signal type to limit the link to. Overrides the calculated intersection of port types for routing calculations.</param>
|
||||
/// <param name="overrideType">The signal type to limit the link to. Overrides DestinationPort.Type for routing calculations.</param>
|
||||
public TieLine(RoutingOutputPort sourcePort, RoutingInputPort destinationPort, eRoutingSignalType overrideType) :
|
||||
this(sourcePort, destinationPort)
|
||||
{
|
||||
|
||||
@@ -49,9 +49,7 @@ namespace PepperDash.Essentials.Core.Config
|
||||
public string DestinationPort { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional override for the signal type of the tie line. If set, this overrides the calculated
|
||||
/// intersection of source and destination port types for routing calculations. Useful when the
|
||||
/// physical cable supports fewer signal types than both ports are capable of.
|
||||
/// Optional override for the signal type of the tie line. If set, this overrides the destination port's type for routing calculations.
|
||||
/// </summary>
|
||||
[JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
|
||||
[JsonConverter(typeof(StringEnumConverter))]
|
||||
@@ -98,32 +96,6 @@ namespace PepperDash.Essentials.Core.Config
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate signal type compatibility
|
||||
if (OverrideType.HasValue)
|
||||
{
|
||||
// When override type is specified, both ports must support it
|
||||
if (!sourceOutputPort.Type.HasFlag(OverrideType.Value))
|
||||
{
|
||||
LogError($"Override type '{OverrideType.Value}' is not supported by source port '{SourcePort}' (type: {sourceOutputPort.Type})");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!destinationInputPort.Type.HasFlag(OverrideType.Value))
|
||||
{
|
||||
LogError($"Override type '{OverrideType.Value}' is not supported by destination port '{DestinationPort}' (type: {destinationInputPort.Type})");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Without override type, ports must have at least one common signal type flag
|
||||
if ((sourceOutputPort.Type & destinationInputPort.Type) == 0)
|
||||
{
|
||||
LogError($"Incompatible signal types: source port '{SourcePort}' (type: {sourceOutputPort.Type}) has no common signal types with destination port '{DestinationPort}' (type: {destinationInputPort.Type})");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return new TieLine(sourceOutputPort, destinationInputPort, OverrideType);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,71 +10,71 @@ 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("versions")
|
||||
@@ -177,11 +177,6 @@ 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);
|
||||
@@ -216,79 +211,78 @@ 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 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));
|
||||
|
||||
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);
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Crestron.SimplSharp.WebScripting;
|
||||
using Newtonsoft.Json;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Core.Web.RequestHandlers;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles HTTP requests to retrieve routing devices and tielines information
|
||||
/// </summary>
|
||||
public class GetRoutingDevicesAndTieLinesHandler : WebApiBaseRequestHandler
|
||||
{
|
||||
public GetRoutingDevicesAndTieLinesHandler() : base(true) { }
|
||||
|
||||
protected override void HandleGet(HttpCwsContext context)
|
||||
{
|
||||
var devices = new List<RoutingDeviceInfo>();
|
||||
|
||||
// Get all devices from DeviceManager
|
||||
foreach (var device in DeviceManager.AllDevices)
|
||||
{
|
||||
var deviceInfo = new RoutingDeviceInfo
|
||||
{
|
||||
Key = device.Key,
|
||||
Name = (device as IKeyName)?.Name ?? device.Key
|
||||
};
|
||||
|
||||
// Check if device implements IRoutingInputs
|
||||
if (device is IRoutingInputs inputDevice)
|
||||
{
|
||||
deviceInfo.HasInputs = true;
|
||||
deviceInfo.InputPorts = inputDevice.InputPorts.Select(p => new PortInfo
|
||||
{
|
||||
Key = p.Key,
|
||||
SignalType = p.Type.ToString(),
|
||||
ConnectionType = p.ConnectionType.ToString(),
|
||||
IsInternal = p.IsInternal
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
// Check if device implements IRoutingOutputs
|
||||
if (device is IRoutingOutputs outputDevice)
|
||||
{
|
||||
deviceInfo.HasOutputs = true;
|
||||
deviceInfo.OutputPorts = outputDevice.OutputPorts.Select(p => new PortInfo
|
||||
{
|
||||
Key = p.Key,
|
||||
SignalType = p.Type.ToString(),
|
||||
ConnectionType = p.ConnectionType.ToString(),
|
||||
IsInternal = p.IsInternal
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
// Check if device implements IRoutingInputsOutputs
|
||||
if (device is IRoutingInputsOutputs)
|
||||
{
|
||||
deviceInfo.HasInputsAndOutputs = true;
|
||||
}
|
||||
|
||||
// Only include devices that have routing capabilities
|
||||
if (deviceInfo.HasInputs || deviceInfo.HasOutputs)
|
||||
{
|
||||
devices.Add(deviceInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Get all tielines
|
||||
var tielines = TieLineCollection.Default.Select(tl => new TieLineInfo
|
||||
{
|
||||
SourceDeviceKey = tl.SourcePort.ParentDevice.Key,
|
||||
SourcePortKey = tl.SourcePort.Key,
|
||||
DestinationDeviceKey = tl.DestinationPort.ParentDevice.Key,
|
||||
DestinationPortKey = tl.DestinationPort.Key,
|
||||
SignalType = tl.Type.ToString(),
|
||||
IsInternal = tl.IsInternal
|
||||
}).ToList();
|
||||
|
||||
var response = new RoutingSystemInfo
|
||||
{
|
||||
Devices = devices,
|
||||
TieLines = tielines
|
||||
};
|
||||
|
||||
var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented);
|
||||
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.StatusDescription = "OK";
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.ContentEncoding = Encoding.UTF8;
|
||||
context.Response.Write(jsonResponse, false);
|
||||
context.Response.End();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the complete routing system information including devices and tielines
|
||||
/// </summary>
|
||||
public class RoutingSystemInfo
|
||||
{
|
||||
[JsonProperty("devices")]
|
||||
public List<RoutingDeviceInfo> Devices { get; set; }
|
||||
|
||||
[JsonProperty("tieLines")]
|
||||
public List<TieLineInfo> TieLines { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a routing device with its ports information
|
||||
/// </summary>
|
||||
public class RoutingDeviceInfo
|
||||
{
|
||||
[JsonProperty("key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("hasInputs")]
|
||||
public bool HasInputs { get; set; }
|
||||
|
||||
[JsonProperty("hasOutputs")]
|
||||
public bool HasOutputs { get; set; }
|
||||
|
||||
[JsonProperty("hasInputsAndOutputs")]
|
||||
public bool HasInputsAndOutputs { get; set; }
|
||||
|
||||
[JsonProperty("inputPorts", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<PortInfo> InputPorts { get; set; }
|
||||
|
||||
[JsonProperty("outputPorts", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public List<PortInfo> OutputPorts { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a routing port with its properties
|
||||
/// </summary>
|
||||
public class PortInfo
|
||||
{
|
||||
[JsonProperty("key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
[JsonProperty("signalType")]
|
||||
public string SignalType { get; set; }
|
||||
|
||||
[JsonProperty("connectionType")]
|
||||
public string ConnectionType { get; set; }
|
||||
|
||||
[JsonProperty("isInternal")]
|
||||
public bool IsInternal { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a tieline connection between two ports
|
||||
/// </summary>
|
||||
public class TieLineInfo
|
||||
{
|
||||
[JsonProperty("sourceDeviceKey")]
|
||||
public string SourceDeviceKey { get; set; }
|
||||
|
||||
[JsonProperty("sourcePortKey")]
|
||||
public string SourcePortKey { get; set; }
|
||||
|
||||
[JsonProperty("destinationDeviceKey")]
|
||||
public string DestinationDeviceKey { get; set; }
|
||||
|
||||
[JsonProperty("destinationPortKey")]
|
||||
public string DestinationPortKey { get; set; }
|
||||
|
||||
[JsonProperty("signalType")]
|
||||
public string SignalType { get; set; }
|
||||
|
||||
[JsonProperty("isInternal")]
|
||||
public bool IsInternal { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
@@ -93,16 +92,12 @@ namespace PepperDash.Essentials
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(s => Debug.LogMessage(LogEventLevel.Information, "CONSOLE MESSAGE: {0}", s), "appdebugmessage", "Writes message to log", ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(ListTieLines,
|
||||
"listtielines", "Prints out all tie lines. Usage: listtielines [signaltype]", ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(VisualizeRoutes, "visualizeroutes",
|
||||
"Visualizes routes by signal type",
|
||||
ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(VisualizeCurrentRoutes, "visualizecurrentroutes",
|
||||
"Visualizes current active routes from DefaultCollection",
|
||||
ConsoleAccessLevelEnum.AccessOperator);
|
||||
CrestronConsole.AddNewConsoleCommand(s =>
|
||||
{
|
||||
foreach (var tl in TieLineCollection.Default)
|
||||
CrestronConsole.ConsoleCommandResponse(" {0}{1}", tl, CrestronEnvironment.NewLine);
|
||||
},
|
||||
"listtielines", "Prints out all tie lines", ConsoleAccessLevelEnum.AccessOperator);
|
||||
|
||||
CrestronConsole.AddNewConsoleCommand(s =>
|
||||
{
|
||||
@@ -448,282 +443,6 @@ namespace PepperDash.Essentials
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user