mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 19:08:29 +00:00
feat: enhance routing graph handling for tile-sink children and add RoutingGraphHelpers utility class
This commit is contained in:
parent
7d1e0432cb
commit
079f2beb8b
4 changed files with 335 additions and 44 deletions
|
|
@ -5,6 +5,7 @@ using Crestron.SimplSharp.WebScripting;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using PepperDash.Core;
|
using PepperDash.Core;
|
||||||
using PepperDash.Core.Web.RequestHandlers;
|
using PepperDash.Core.Web.RequestHandlers;
|
||||||
|
using PepperDash.Essentials.Core.Web;
|
||||||
|
|
||||||
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
{
|
{
|
||||||
|
|
@ -27,9 +28,17 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
{
|
{
|
||||||
var devices = new List<RoutingDeviceInfo>();
|
var devices = new List<RoutingDeviceInfo>();
|
||||||
|
|
||||||
|
// Tile-sink children of an IRoutingSinkWithLayouts device (e.g. a multiview decoder's
|
||||||
|
// per-window sinks) are rendered as synthesized input ports on the parent's node instead
|
||||||
|
// of as their own separate nodes - see BuildRoutingDeviceInfo below.
|
||||||
|
var tileChildren = RoutingGraphHelpers.BuildTileChildMap();
|
||||||
|
|
||||||
// Get all devices from DeviceManager
|
// Get all devices from DeviceManager
|
||||||
foreach (var device in DeviceManager.AllDevices)
|
foreach (var device in DeviceManager.AllDevices)
|
||||||
{
|
{
|
||||||
|
if (tileChildren.ContainsKey(device.Key))
|
||||||
|
continue;
|
||||||
|
|
||||||
var deviceInfo = new RoutingDeviceInfo
|
var deviceInfo = new RoutingDeviceInfo
|
||||||
{
|
{
|
||||||
Key = device.Key,
|
Key = device.Key,
|
||||||
|
|
@ -68,6 +77,27 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
deviceInfo.HasInputsAndOutputs = true;
|
deviceInfo.HasInputsAndOutputs = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Devices implementing IRoutingSinkWithLayouts (e.g. a multiview decoder) don't
|
||||||
|
// implement IRoutingInputs themselves - their tiles do. Synthesize one input port per
|
||||||
|
// tile (qualified so multiple tiles don't collide) so the graph can render a single
|
||||||
|
// node with one edge-target input per tile.
|
||||||
|
if (device is IRoutingSinkWithLayouts layoutDevice)
|
||||||
|
{
|
||||||
|
deviceInfo.HasInputs = true;
|
||||||
|
|
||||||
|
var tilePorts = layoutDevice.WindowTileSinks
|
||||||
|
.OrderBy(kvp => kvp.Key)
|
||||||
|
.SelectMany(kvp => (kvp.Value as IRoutingInputs)?.InputPorts.Select(p => new PortInfo
|
||||||
|
{
|
||||||
|
Key = RoutingGraphHelpers.QualifyTilePortKey(kvp.Key, p.Key),
|
||||||
|
SignalType = p.Type.ToString(),
|
||||||
|
ConnectionType = p.ConnectionType.ToString(),
|
||||||
|
IsInternal = p.IsInternal
|
||||||
|
}) ?? []);
|
||||||
|
|
||||||
|
deviceInfo.InputPorts = (deviceInfo.InputPorts ?? []).Concat(tilePorts).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
// Only include devices that have routing capabilities
|
// Only include devices that have routing capabilities
|
||||||
if (deviceInfo.HasInputs || deviceInfo.HasOutputs)
|
if (deviceInfo.HasInputs || deviceInfo.HasOutputs)
|
||||||
{
|
{
|
||||||
|
|
@ -75,15 +105,28 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all tielines
|
// Get all tielines, remapping any that target a tile-sink child so they point at its
|
||||||
var tielines = TieLineCollection.Default.Select(tl => new TieLineInfo
|
// IRoutingSinkWithLayouts parent's node/qualified port instead.
|
||||||
|
var tielines = TieLineCollection.Default.Select(tl =>
|
||||||
|
{
|
||||||
|
var destinationDeviceKey = tl.DestinationPort.ParentDevice.Key;
|
||||||
|
var destinationPortKey = tl.DestinationPort.Key;
|
||||||
|
|
||||||
|
if (tileChildren.TryGetValue(destinationDeviceKey, out var tileInfo))
|
||||||
|
{
|
||||||
|
destinationDeviceKey = tileInfo.Parent.Key;
|
||||||
|
destinationPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, destinationPortKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TieLineInfo
|
||||||
{
|
{
|
||||||
SourceDeviceKey = tl.SourcePort.ParentDevice.Key,
|
SourceDeviceKey = tl.SourcePort.ParentDevice.Key,
|
||||||
SourcePortKey = tl.SourcePort.Key,
|
SourcePortKey = tl.SourcePort.Key,
|
||||||
DestinationDeviceKey = tl.DestinationPort.ParentDevice.Key,
|
DestinationDeviceKey = destinationDeviceKey,
|
||||||
DestinationPortKey = tl.DestinationPort.Key,
|
DestinationPortKey = destinationPortKey,
|
||||||
SignalType = tl.Type.ToString(),
|
SignalType = tl.Type.ToString(),
|
||||||
IsInternal = tl.IsInternal
|
IsInternal = tl.IsInternal
|
||||||
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
// Get current active routes from DefaultCollection, grouped by signal type
|
// Get current active routes from DefaultCollection, grouped by signal type
|
||||||
|
|
@ -92,17 +135,43 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
.Select(g => new CurrentRouteGroupInfo
|
.Select(g => new CurrentRouteGroupInfo
|
||||||
{
|
{
|
||||||
SignalType = g.Key,
|
SignalType = g.Key,
|
||||||
Routes = [.. g.Select(d => new ActiveRouteInfo
|
Routes = [.. g.Select(d =>
|
||||||
|
{
|
||||||
|
var destinationDeviceKey = d.Destination.Key;
|
||||||
|
var destinationInputPortKey = d.InputPort?.Key;
|
||||||
|
|
||||||
|
if (tileChildren.TryGetValue(destinationDeviceKey, out var tileInfo))
|
||||||
|
{
|
||||||
|
destinationDeviceKey = tileInfo.Parent.Key;
|
||||||
|
if (destinationInputPortKey != null)
|
||||||
|
destinationInputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, destinationInputPortKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ActiveRouteInfo
|
||||||
{
|
{
|
||||||
SourceDeviceKey = d.Source.Key,
|
SourceDeviceKey = d.Source.Key,
|
||||||
DestinationDeviceKey = d.Destination.Key,
|
DestinationDeviceKey = destinationDeviceKey,
|
||||||
DestinationInputPortKey = d.InputPort?.Key,
|
DestinationInputPortKey = destinationInputPortKey,
|
||||||
Steps = [.. d.Routes.Select(r => new RouteSwitchStepInfo
|
Steps = [.. d.Routes.Select(r =>
|
||||||
{
|
{
|
||||||
SwitchingDeviceKey = r.SwitchingDevice?.Key,
|
var switchingDeviceKey = r.SwitchingDevice?.Key;
|
||||||
InputPortKey = r.InputPort?.Key,
|
var inputPortKey = r.InputPort?.Key;
|
||||||
|
|
||||||
|
if (switchingDeviceKey != null && tileChildren.TryGetValue(switchingDeviceKey, out var stepTileInfo))
|
||||||
|
{
|
||||||
|
switchingDeviceKey = stepTileInfo.Parent.Key;
|
||||||
|
if (inputPortKey != null)
|
||||||
|
inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(stepTileInfo.TileNumber, inputPortKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RouteSwitchStepInfo
|
||||||
|
{
|
||||||
|
SwitchingDeviceKey = switchingDeviceKey,
|
||||||
|
InputPortKey = inputPortKey,
|
||||||
OutputPortKey = r.OutputPort?.Key
|
OutputPortKey = r.OutputPort?.Key
|
||||||
|
};
|
||||||
})]
|
})]
|
||||||
|
};
|
||||||
})]
|
})]
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
|
|
@ -110,7 +179,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
{
|
{
|
||||||
Devices = devices,
|
Devices = devices,
|
||||||
TieLines = tielines,
|
TieLines = tielines,
|
||||||
CurrentRoutes = currentRoutes
|
CurrentRoutes = currentRoutes,
|
||||||
|
SinkCurrentSources = BuildSinkCurrentSources(tileChildren)
|
||||||
};
|
};
|
||||||
|
|
||||||
var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented);
|
var jsonResponse = JsonConvert.SerializeObject(response, Formatting.Indented);
|
||||||
|
|
@ -122,6 +192,50 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
context.Response.Write(jsonResponse, false);
|
context.Response.Write(jsonResponse, false);
|
||||||
context.Response.End();
|
context.Response.End();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds current-source info for every sink device, read directly from each sink's own
|
||||||
|
/// <see cref="ICurrentSources"/> bookkeeping (see <see cref="RoutingGraphHelpers.GetCurrentSourceKey"/>).
|
||||||
|
/// Unlike <see cref="RouteDescriptorCollection"/>-based <see cref="CurrentRouteGroupInfo"/>, this
|
||||||
|
/// also reflects routes made via a device-specific bulk API (e.g.
|
||||||
|
/// <c>IHasDynamicMultiviewLayout.ApplyDynamicLayout</c>) that never creates a
|
||||||
|
/// <see cref="RouteDescriptor"/> or <see cref="TieLine"/> at all, so it's what the dev tools app
|
||||||
|
/// should use to seed initial sink-routing state on page load.
|
||||||
|
/// </summary>
|
||||||
|
private static List<SinkCurrentSourceInfo> BuildSinkCurrentSources(
|
||||||
|
Dictionary<string, RoutingGraphHelpers.TileChildInfo> tileChildren)
|
||||||
|
{
|
||||||
|
var result = new List<SinkCurrentSourceInfo>();
|
||||||
|
|
||||||
|
foreach (var device in DeviceManager.AllDevices.OfType<IRoutingSinkWithFeedback>())
|
||||||
|
{
|
||||||
|
if (device.CurrentInputPort == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var sourceKey = RoutingGraphHelpers.GetCurrentSourceKey(device);
|
||||||
|
if (string.IsNullOrEmpty(sourceKey))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var deviceKey = device.Key;
|
||||||
|
var inputPortKey = device.CurrentInputPort.Key;
|
||||||
|
|
||||||
|
if (tileChildren.TryGetValue(deviceKey, out var tileInfo))
|
||||||
|
{
|
||||||
|
deviceKey = tileInfo.Parent.Key;
|
||||||
|
inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, inputPortKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(new SinkCurrentSourceInfo
|
||||||
|
{
|
||||||
|
DeviceKey = deviceKey,
|
||||||
|
InputPortKey = inputPortKey,
|
||||||
|
SourceDeviceKey = sourceKey,
|
||||||
|
SignalType = device.CurrentInputPort.Type.ToString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -148,6 +262,44 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("currentRoutes")]
|
[JsonProperty("currentRoutes")]
|
||||||
public List<CurrentRouteGroupInfo> CurrentRoutes { get; set; }
|
public List<CurrentRouteGroupInfo> CurrentRoutes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the current source feeding each sink device, read directly from each sink's own
|
||||||
|
/// current-source bookkeeping. Covers routes made via device-specific bulk APIs (e.g.
|
||||||
|
/// dynamic multiview layouts) that <see cref="CurrentRoutes"/> does not.
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("sinkCurrentSources")]
|
||||||
|
public List<SinkCurrentSourceInfo> SinkCurrentSources { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents the source currently feeding a single sink device's input port
|
||||||
|
/// </summary>
|
||||||
|
public class SinkCurrentSourceInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the key of the sink device (or its IRoutingSinkWithLayouts parent, if this is a tile)
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("deviceKey")]
|
||||||
|
public string DeviceKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the key of the input port currently receiving the source
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("inputPortKey")]
|
||||||
|
public string InputPortKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the key of the device currently feeding this input
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("sourceDeviceKey")]
|
||||||
|
public string SourceDeviceKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the signal type of the input port (e.g., AudioVideo, Audio, Video, etc.)
|
||||||
|
/// </summary>
|
||||||
|
[JsonProperty("signalType")]
|
||||||
|
public string SignalType { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@ public class RoutingFeedbackWebsocket : IKeyed
|
||||||
|
|
||||||
private readonly Dictionary<string, Timer> _debounceTimers = new Dictionary<string, Timer>();
|
private readonly Dictionary<string, Timer> _debounceTimers = new Dictionary<string, Timer>();
|
||||||
|
|
||||||
|
// Tile-sink children of an IRoutingSinkWithLayouts device (e.g. a multiview decoder's per-window
|
||||||
|
// sinks) are reported to clients as synthesized inputs on the parent's node, rather than as their
|
||||||
|
// own separate nodes - see RoutingGraphHelpers. Rebuilt whenever the server (re)starts.
|
||||||
|
private Dictionary<string, RoutingGraphHelpers.TileChildInfo> _tileChildren = new Dictionary<string, RoutingGraphHelpers.TileChildInfo>();
|
||||||
|
|
||||||
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
|
private static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
|
||||||
{
|
{
|
||||||
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
ContractResolver = new CamelCasePropertyNamesContractResolver(),
|
||||||
|
|
@ -157,7 +162,7 @@ public class RoutingFeedbackWebsocket : IKeyed
|
||||||
internal string GetSnapshotMessage()
|
internal string GetSnapshotMessage()
|
||||||
{
|
{
|
||||||
var midpointRoutes = new Dictionary<string, List<MidpointRouteDto>>();
|
var midpointRoutes = new Dictionary<string, List<MidpointRouteDto>>();
|
||||||
var sinkRoutes = new Dictionary<string, SinkRouteDto>();
|
var sinkRoutes = new Dictionary<string, List<SinkRouteDto>>();
|
||||||
|
|
||||||
// Collect midpoint current routes
|
// Collect midpoint current routes
|
||||||
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingMidpointWithFeedback>();
|
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingMidpointWithFeedback>();
|
||||||
|
|
@ -177,27 +182,42 @@ public class RoutingFeedbackWebsocket : IKeyed
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect sink current sources
|
// Collect sink current sources directly from each sink's own current-source bookkeeping
|
||||||
|
// (ICurrentSources, part of IRoutingSinkWithFeedback), which is authoritative regardless of
|
||||||
|
// whether the route was made via a tie line (ReleaseAndMakeRoute) or a device-specific bulk
|
||||||
|
// API (e.g. IHasDynamicMultiviewLayout.ApplyDynamicLayout) that never touches
|
||||||
|
// TieLineCollection/RouteDescriptorCollection at all.
|
||||||
var sinkDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithFeedback>();
|
var sinkDevices = DeviceManager.AllDevices.OfType<IRoutingSinkWithFeedback>();
|
||||||
foreach (var device in sinkDevices)
|
foreach (var device in sinkDevices)
|
||||||
{
|
{
|
||||||
if (device.CurrentInputPort == null)
|
if (device.CurrentInputPort == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// Trace back to find source
|
var sourceKey = RoutingGraphHelpers.GetCurrentSourceKey(device);
|
||||||
var tieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
if (string.IsNullOrEmpty(sourceKey))
|
||||||
tl.DestinationPort.Key == device.CurrentInputPort.Key &&
|
continue;
|
||||||
tl.DestinationPort.ParentDevice.Key == device.CurrentInputPort.ParentDevice.Key);
|
|
||||||
|
|
||||||
if (tieLine != null)
|
var deviceKey = device.Key;
|
||||||
|
var inputPortKey = device.CurrentInputPort.Key;
|
||||||
|
|
||||||
|
if (_tileChildren.TryGetValue(deviceKey, out var tileInfo))
|
||||||
{
|
{
|
||||||
sinkRoutes[device.Key] = new SinkRouteDto
|
deviceKey = tileInfo.Parent.Key;
|
||||||
{
|
inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, inputPortKey);
|
||||||
InputPortKey = device.CurrentInputPort.Key,
|
|
||||||
SourceDeviceKey = tieLine.SourcePort.ParentDevice.Key,
|
|
||||||
SignalType = device.CurrentInputPort.Type.ToString()
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sinkRoutes.TryGetValue(deviceKey, out var routes))
|
||||||
|
{
|
||||||
|
routes = new List<SinkRouteDto>();
|
||||||
|
sinkRoutes[deviceKey] = routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
routes.Add(new SinkRouteDto
|
||||||
|
{
|
||||||
|
InputPortKey = inputPortKey,
|
||||||
|
SourceDeviceKey = sourceKey,
|
||||||
|
SignalType = device.CurrentInputPort.Type.ToString()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = new RoutingSnapshotDto
|
var snapshot = new RoutingSnapshotDto
|
||||||
|
|
@ -212,6 +232,8 @@ public class RoutingFeedbackWebsocket : IKeyed
|
||||||
|
|
||||||
private void SubscribeToRoutingEvents()
|
private void SubscribeToRoutingEvents()
|
||||||
{
|
{
|
||||||
|
_tileChildren = RoutingGraphHelpers.BuildTileChildMap();
|
||||||
|
|
||||||
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingMidpointWithFeedback>();
|
var midpointDevices = DeviceManager.AllDevices.OfType<IRoutingMidpointWithFeedback>();
|
||||||
foreach (var device in midpointDevices)
|
foreach (var device in midpointDevices)
|
||||||
{
|
{
|
||||||
|
|
@ -267,22 +289,31 @@ public class RoutingFeedbackWebsocket : IKeyed
|
||||||
|
|
||||||
private void HandleSinkInputChanged(IRoutingSinkWithFeedback sender, RoutingInputPort currentInputPort)
|
private void HandleSinkInputChanged(IRoutingSinkWithFeedback sender, RoutingInputPort currentInputPort)
|
||||||
{
|
{
|
||||||
|
// Tile-sink children are reported under their IRoutingSinkWithLayouts parent's key, with a
|
||||||
|
// qualified port key, so clients see this as an input change on the parent's node rather than
|
||||||
|
// on a device that isn't otherwise represented in the graph.
|
||||||
|
var deviceKey = sender.Key;
|
||||||
|
var inputPortKey = currentInputPort?.Key ?? "";
|
||||||
|
|
||||||
|
if (_tileChildren.TryGetValue(deviceKey, out var tileInfo))
|
||||||
|
{
|
||||||
|
deviceKey = tileInfo.Parent.Key;
|
||||||
|
if (!string.IsNullOrEmpty(inputPortKey))
|
||||||
|
inputPortKey = RoutingGraphHelpers.QualifyTilePortKey(tileInfo.TileNumber, inputPortKey);
|
||||||
|
}
|
||||||
|
|
||||||
DebounceBroadcast($"sink-{sender.Key}", () =>
|
DebounceBroadcast($"sink-{sender.Key}", () =>
|
||||||
{
|
{
|
||||||
var sourceDeviceKey = "";
|
// Read the source directly from the sink's own current-source bookkeeping (see
|
||||||
if (currentInputPort != null)
|
// GetCurrentSourceKey) rather than tracing a tie line - a route made via a
|
||||||
{
|
// device-specific bulk API (e.g. ApplyDynamicLayout) never creates a tie line at all.
|
||||||
var tieLine = TieLineCollection.Default.FirstOrDefault(tl =>
|
var sourceDeviceKey = currentInputPort != null ? (RoutingGraphHelpers.GetCurrentSourceKey(sender) ?? "") : "";
|
||||||
tl.DestinationPort.Key == currentInputPort.Key &&
|
|
||||||
tl.DestinationPort.ParentDevice.Key == currentInputPort.ParentDevice.Key);
|
|
||||||
sourceDeviceKey = tieLine?.SourcePort.ParentDevice.Key ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
var msg = new SinkInputChangedDto
|
var msg = new SinkInputChangedDto
|
||||||
{
|
{
|
||||||
Type = "sinkInputChanged",
|
Type = "sinkInputChanged",
|
||||||
DeviceKey = sender.Key,
|
DeviceKey = deviceKey,
|
||||||
InputPortKey = currentInputPort?.Key ?? "",
|
InputPortKey = inputPortKey,
|
||||||
SourceDeviceKey = sourceDeviceKey,
|
SourceDeviceKey = sourceDeviceKey,
|
||||||
SignalType = currentInputPort?.Type.ToString() ?? ""
|
SignalType = currentInputPort?.Type.ToString() ?? ""
|
||||||
};
|
};
|
||||||
|
|
@ -360,7 +391,7 @@ public class RoutingFeedbackWebsocket : IKeyed
|
||||||
{
|
{
|
||||||
public string Type { get; set; }
|
public string Type { get; set; }
|
||||||
public Dictionary<string, List<MidpointRouteDto>> MidpointRoutes { get; set; }
|
public Dictionary<string, List<MidpointRouteDto>> MidpointRoutes { get; set; }
|
||||||
public Dictionary<string, SinkRouteDto> SinkRoutes { get; set; }
|
public Dictionary<string, List<SinkRouteDto>> SinkRoutes { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private class MidpointRouteChangedDto
|
private class MidpointRouteChangedDto
|
||||||
|
|
|
||||||
89
src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs
Normal file
89
src/PepperDash.Essentials.Core/Web/RoutingGraphHelpers.cs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using PepperDash.Core;
|
||||||
|
|
||||||
|
namespace PepperDash.Essentials.Core.Web;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared helpers for building routing-graph data for consumers of the routing dev tools
|
||||||
|
/// (<see cref="RequestHandlers.GetRoutingDevicesAndTieLinesHandler"/> and
|
||||||
|
/// <see cref="RoutingFeedbackWebsocket"/>).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Devices that implement <see cref="IRoutingSinkWithLayouts"/> (e.g. a multiview decoder) expose a
|
||||||
|
/// set of per-tile child sink devices (<see cref="IRoutingSinkWithLayouts.WindowTileSinks"/>), each of
|
||||||
|
/// which is independently registered with <see cref="DeviceManager"/> and independently routable.
|
||||||
|
/// Rendered naively, each tile shows up as its own top-level node in the routing graph, which is
|
||||||
|
/// confusing - a multiview decoder with N tiles doesn't look like N separate devices to a user.
|
||||||
|
/// These helpers let routing-graph consumers instead: skip tile-sink children when enumerating
|
||||||
|
/// top-level devices, and represent each tile as a distinctly-keyed input "edge target" on the
|
||||||
|
/// parent's own node (see <see cref="QualifyTilePortKey"/>), remapping any tie line / active route
|
||||||
|
/// that targets a tile so it points at the parent device instead.
|
||||||
|
/// </remarks>
|
||||||
|
public static class RoutingGraphHelpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Describes where a tile-sink child device (<see cref="IRoutingSinkWithLayouts.WindowTileSinks"/>)
|
||||||
|
/// belongs, for remapping it back to its parent's node in routing-graph output.
|
||||||
|
/// </summary>
|
||||||
|
public readonly struct TileChildInfo
|
||||||
|
{
|
||||||
|
/// <summary>The parent device this tile belongs to.</summary>
|
||||||
|
public IRoutingSinkWithLayouts Parent { get; }
|
||||||
|
|
||||||
|
/// <summary>The tile's 1-based window number within the parent's layout.</summary>
|
||||||
|
public int TileNumber { get; }
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the <see cref="TileChildInfo"/> struct.</summary>
|
||||||
|
public TileChildInfo(IRoutingSinkWithLayouts parent, int tileNumber)
|
||||||
|
{
|
||||||
|
Parent = parent;
|
||||||
|
TileNumber = tileNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a map of every tile-sink child device key (across all <see cref="IRoutingSinkWithLayouts"/>
|
||||||
|
/// devices currently in <see cref="DeviceManager"/>) to its parent device and tile number.
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, TileChildInfo> BuildTileChildMap()
|
||||||
|
{
|
||||||
|
var map = new Dictionary<string, TileChildInfo>();
|
||||||
|
|
||||||
|
foreach (var parent in DeviceManager.AllDevices.OfType<IRoutingSinkWithLayouts>())
|
||||||
|
{
|
||||||
|
foreach (var kvp in parent.WindowTileSinks)
|
||||||
|
{
|
||||||
|
if (kvp.Value is not IKeyed tile || string.IsNullOrEmpty(tile.Key))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
map[tile.Key] = new TileChildInfo(parent, kvp.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a graph-unique port key for a tile's port, so multiple tiles synthesized onto the same
|
||||||
|
/// parent node don't collide (every tile's own <c>InputPorts</c> collection typically contains a
|
||||||
|
/// single, identically-keyed port, e.g. "tileInput").
|
||||||
|
/// </summary>
|
||||||
|
public static string QualifyTilePortKey(int tileNumber, string portKey) => $"tile{tileNumber}:{portKey}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the device key of whatever source is currently feeding a sink's Video signal (falling back
|
||||||
|
/// to Audio), read directly from its own <see cref="ICurrentSources"/> bookkeeping (part of
|
||||||
|
/// <see cref="IRoutingSinkWithFeedback"/>). This is authoritative regardless of whether the route
|
||||||
|
/// was made via a tie line (<c>ReleaseAndMakeRoute</c>) or a device-specific bulk API (e.g.
|
||||||
|
/// <c>IHasDynamicMultiviewLayout.ApplyDynamicLayout</c>) that never touches
|
||||||
|
/// <c>TieLineCollection</c>/<c>RouteDescriptorCollection</c> at all.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetCurrentSourceKey(IRoutingSinkWithFeedback device)
|
||||||
|
{
|
||||||
|
if (device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Video, out var videoKey) && !string.IsNullOrEmpty(videoKey))
|
||||||
|
return videoKey;
|
||||||
|
|
||||||
|
return device.CurrentSourceKeys.TryGetValue(eRoutingSignalType.Audio, out var audioKey) ? audioKey : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1305,8 +1305,17 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
}
|
}
|
||||||
byte[] contents = File.ReadAllBytes(filePath);
|
byte[] contents = File.ReadAllBytes(filePath);
|
||||||
res.ContentLength64 = contents.LongLength;
|
res.ContentLength64 = contents.LongLength;
|
||||||
|
try
|
||||||
|
{
|
||||||
res.Close(contents, true);
|
res.Close(contents, true);
|
||||||
}
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
// Client disconnected (e.g. panel refreshed/navigated away) before the response could be
|
||||||
|
// fully written. This is a benign, expected condition, not an application error.
|
||||||
|
this.LogVerbose("Client disconnected before image response could be sent: {message}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
res.StatusCode = (int)HttpStatusCode.NotFound;
|
res.StatusCode = (int)HttpStatusCode.NotFound;
|
||||||
|
|
@ -1427,8 +1436,18 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
}
|
}
|
||||||
|
|
||||||
res.ContentLength64 = contents.LongLength;
|
res.ContentLength64 = contents.LongLength;
|
||||||
|
try
|
||||||
|
{
|
||||||
res.Close(contents, true);
|
res.Close(contents, true);
|
||||||
}
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
// Client disconnected (e.g. panel refreshed/navigated away, or made a duplicate request)
|
||||||
|
// before the response could be fully written. This is a benign, expected condition
|
||||||
|
// (e.g. a "Broken pipe" IOException), not an application error worth logging at Error level.
|
||||||
|
this.LogVerbose("Client disconnected before user app response could be sent: {message}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// StopServer method
|
/// StopServer method
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue