Compare commits

..

6 Commits

Author SHA1 Message Date
equinoy
c0971a4f8e Merge remote-tracking branch 'origin/screen-lift-controller-mute-logic' into feature/circuittype-property-versiport 2026-01-22 15:36:27 -06:00
equinoy
8bc6d4392b feat: add circuitType property to IOPortConfig and implement state inversion in digital input devices 2026-01-22 15:04:25 -06:00
Neil Dorin
4fa7a42330 Merge pull request #1376 from PepperDash/client-id-issues
fix: handle subsequent join calls and clientid/websocket client mismatches
2026-01-21 14:59:22 -07:00
Andrew Welker
9bad3ae21b fix: handle subsequent join calls and clientid/websocket client mismatches 2026-01-21 15:20:27 -06:00
Nick Genovese
f49901d3fa fix: Improve status messages in StatusMonitorCollection
Enhanced error and warning message generation to use monitor names when available, include counts, proper pluralization, and append "Offline" when issues are present. Avoided multiple enumerations by converting to lists. "Room Ok." is shown when no issues are detected.
2026-01-01 18:01:37 -06:00
Nick Genovese
7910b7931e feat: Add mute logic to ScreenLiftController
- adds a config value that mutes the display when the screen is in the up position
- screens will now mute/unmute based on their position if the config is set
2026-01-01 09:02:04 -06:00
17 changed files with 760 additions and 1339 deletions

View File

@@ -21,6 +21,7 @@ namespace PepperDash.Essentials.Core.CrestronIO
public class GenericDigitalInputDevice : EssentialsBridgeableDevice, IDigitalInput, IHasFeedback
{
private DigitalInput inputPort;
private bool invertState;
/// <summary>
/// Gets or sets the InputStateFeedback
@@ -41,7 +42,10 @@ namespace PepperDash.Essentials.Core.CrestronIO
IOPortConfig config)
: base(key, name)
{
InputStateFeedback = new BoolFeedback("inputState", () => inputPort.State);
invertState = !string.IsNullOrEmpty(config.CircuitType) &&
config.CircuitType.Equals("NC", StringComparison.OrdinalIgnoreCase);
InputStateFeedback = new BoolFeedback("inputState", () => invertState ? !inputPort.State : inputPort.State);
AddPostActivationAction(() =>
{

View File

@@ -18,6 +18,7 @@ namespace PepperDash.Essentials.Core.CrestronIO
public class GenericVersiportDigitalInputDevice : EssentialsBridgeableDevice, IDigitalInput, IPartitionStateProvider, IHasFeedback
{
private Versiport inputPort;
private bool invertState;
/// <summary>
/// Gets or sets the InputStateFeedback
@@ -47,7 +48,10 @@ namespace PepperDash.Essentials.Core.CrestronIO
public GenericVersiportDigitalInputDevice(string key, string name, Func<IOPortConfig, Versiport> postActivationFunc, IOPortConfig config) :
base(key, name)
{
InputStateFeedback = new BoolFeedback("inputState", () => inputPort.DigitalIn);
invertState = !string.IsNullOrEmpty(config.CircuitType) &&
config.CircuitType.Equals("NC", StringComparison.OrdinalIgnoreCase);
InputStateFeedback = new BoolFeedback("inputState", () => invertState ? !inputPort.DigitalIn : inputPort.DigitalIn);
PartitionPresentFeedback = new BoolFeedback("partitionPresent", () => !inputPort.DigitalIn);
AddPostActivationAction(() =>

View File

@@ -37,5 +37,12 @@ namespace PepperDash.Essentials.Core.CrestronIO
/// </summary>
[JsonProperty("minimumChange")]
public int MinimumChange { get; set; }
/// <summary>
/// Gets or sets the circuit type: "NO" (Normally Open) or "NC" (Normally Closed)
/// If set to "NC", the input state will be inverted. Defaults to "NO" if not specified.
/// </summary>
[JsonProperty("circuitType")]
public string CircuitType { get; set; } = "NO";
}
}

View File

@@ -436,14 +436,14 @@ namespace PepperDash.Essentials.Core
CrestronConsole.ConsoleCommandResponse("Device {0} has {1} Input Ports:{2}", s, inputPorts.Count, CrestronEnvironment.NewLine);
foreach (var routingInputPort in inputPorts)
{
CrestronConsole.ConsoleCommandResponse("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);
}
}

View File

@@ -14,7 +14,7 @@ using PepperDash.Core;
namespace PepperDash.Essentials.Core
{
/// <summary>
///
///
/// </summary>
public class StatusMonitorCollection : IStatusMonitor
{
@@ -59,51 +59,61 @@ namespace PepperDash.Essentials.Core
void ProcessStatuses()
{
var InError = Monitors.Where(m => m.Status == MonitorStatus.InError);
var InWarning = Monitors.Where(m => m.Status == MonitorStatus.InWarning);
var IsOk = Monitors.Where(m => m.Status == MonitorStatus.IsOk);
var InError = Monitors.Where(m => m.Status == MonitorStatus.InError).ToList();
var InWarning = Monitors.Where(m => m.Status == MonitorStatus.InWarning).ToList();
var IsOk = Monitors.Where(m => m.Status == MonitorStatus.IsOk).ToList();
MonitorStatus initialStatus;
string prefix = "0:";
if (InError.Count() > 0)
if (InError.Any())
{
initialStatus = MonitorStatus.InError;
prefix = "3:";
}
else if (InWarning.Count() > 0)
else if (InWarning.Any())
{
initialStatus = MonitorStatus.InWarning;
prefix = "2:";
}
else if (IsOk.Count() > 0)
else if (IsOk.Any())
initialStatus = MonitorStatus.IsOk;
else
initialStatus = MonitorStatus.StatusUnknown;
// Build the error message string
if (InError.Count() > 0 || InWarning.Count() > 0)
{
StringBuilder sb = new StringBuilder(prefix);
if (InError.Count() > 0)
{
// Do string splits and joins
sb.Append(string.Format("{0} Errors:", InError.Count()));
foreach (var mon in InError)
sb.Append(string.Format("{0}, ", mon.Parent.Key));
}
if (InWarning.Count() > 0)
{
sb.Append(string.Format("{0} Warnings:", InWarning.Count()));
foreach (var mon in InWarning)
sb.Append(string.Format("{0}, ", mon.Parent.Key));
}
Message = sb.ToString();
}
else
{
Message = "Room Ok.";
}
if (InError.Any() || InWarning.Any())
{
var errorNames = InError
.Select(mon => mon.Parent is IKeyName keyName ? keyName.Name : mon.Parent.Key)
.ToList();
var warningNames = InWarning
.Select(mon => mon.Parent is IKeyName keyName ? keyName.Name : mon.Parent.Key)
.ToList();
var sb = new StringBuilder(prefix);
if (errorNames.Count > 0)
{
sb.Append($"{errorNames.Count} Error{(errorNames.Count > 1 ? "s" : "")}: ");
sb.Append(string.Join(", ", errorNames));
}
if (warningNames.Count > 0)
{
if (errorNames.Count > 0)
sb.Append("; ");
sb.Append($"{warningNames.Count} Warning{(warningNames.Count > 1 ? "s" : "")}: ");
sb.Append(string.Join(", ", warningNames));
}
sb.Append(" Offline");
Message = sb.ToString();
}
else
{
Message = "Room Ok.";
}
// Want to fire even if status doesn't change because the message may.
Status = initialStatus;

View File

@@ -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,11 @@ 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>
/// <returns>true if source is hit</returns>
private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOutputs source,
RoutingOutputPort outputPortToUse, List<IRoutingInputsOutputs> alreadyCheckedDevices,
@@ -577,54 +333,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 +423,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;
}

View File

@@ -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));
}
}*/
}

View File

@@ -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
@@ -11,9 +11,6 @@ namespace PepperDash.Essentials.Core
/// </summary>
public class RouteDescriptorCollection
{
/// <summary>
/// Gets the default collection of RouteDescriptors.
/// </summary>
public static RouteDescriptorCollection DefaultCollection
{
get
@@ -27,11 +24,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 +37,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);
}
@@ -81,12 +57,6 @@ namespace PepperDash.Essentials.Core
return RouteDescriptors.FirstOrDefault(rd => rd.Destination == destination);
}
/// <summary>
/// Gets the route descriptor for a specific destination and input port
/// </summary>
/// <param name="destination">The destination device</param>
/// <param name="inputPortKey">The input port key</param>
/// <returns>The matching RouteDescriptor or null if not found</returns>
public RouteDescriptor GetRouteDescriptorForDestinationAndInputPort(IRoutingInputs destination, string inputPortKey)
{
Debug.LogMessage(LogEventLevel.Information, "Getting route descriptor for '{destination}':'{inputPortKey}'", destination?.Key ?? null, string.IsNullOrEmpty(inputPortKey) ? "auto" : inputPortKey);
@@ -103,7 +73,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 +84,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;
}
}*/
}

View File

@@ -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);
}
}
}*/
}

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}
}

View File

@@ -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; }
}
}

View File

@@ -19,7 +19,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{
None,
Raise,
Lower
Lower,
}
/// <summary>
@@ -50,7 +50,8 @@ namespace PepperDash.Essentials.Devices.Common.Shades
get { return _isInUpPosition; }
set
{
if (value == _isInUpPosition) return;
if (value == _isInUpPosition)
return;
_isInUpPosition = value;
IsInUpPosition.FireUpdate();
PositionChanged?.Invoke(this, new EventArgs());
@@ -87,7 +88,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// <summary>
/// Constructor for ScreenLiftController
/// </summary>
public ScreenLiftController(string key, string name, ScreenLiftControllerConfigProperties config)
public ScreenLiftController(
string key,
string name,
ScreenLiftControllerConfigProperties config
)
: base(key, name)
{
Config = config;
@@ -105,27 +110,60 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
RaiseRelayConfig = Config.Relays["raise"];
LowerRelayConfig = Config.Relays["lower"];
break;
}
{
RaiseRelayConfig = Config.Relays["raise"];
LowerRelayConfig = Config.Relays["lower"];
break;
}
case eScreenLiftControlMode.latched:
{
LatchedRelayConfig = Config.Relays["latched"];
break;
}
{
LatchedRelayConfig = Config.Relays["latched"];
break;
}
}
IsInUpPosition.OutputChange += (sender, args) =>
{
this.LogDebug(
"ScreenLiftController '{name}' IsInUpPosition changed to {position}",
Name,
IsInUpPosition.BoolValue ? "Up" : "Down"
);
if (!Config.MuteOnScreenUp)
{
return;
}
if (args.BoolValue)
{
return;
}
if (DisplayDevice is IBasicVideoMuteWithFeedback videoMute)
{
this.LogInformation("Unmuting video because screen is down");
videoMute.VideoMuteOff();
}
};
IsInUpPosition.FireUpdate();
}
private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e)
{
if (!DisplayDevice.IsCoolingDownFeedback.BoolValue && Type == eScreenLiftControlType.lift)
if (
!DisplayDevice.IsCoolingDownFeedback.BoolValue
&& Type == eScreenLiftControlType.lift
)
{
Raise();
return;
}
if (DisplayDevice.IsCoolingDownFeedback.BoolValue && Type == eScreenLiftControlType.screen)
if (
DisplayDevice.IsCoolingDownFeedback.BoolValue
&& Type == eScreenLiftControlType.screen
)
{
Raise();
return;
@@ -150,18 +188,18 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
this.LogDebug("Getting relays for {mode}", Mode);
RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey);
LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey);
break;
}
{
this.LogDebug("Getting relays for {mode}", Mode);
RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey);
LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey);
break;
}
case eScreenLiftControlMode.latched:
{
this.LogDebug("Getting relays for {mode}", Mode);
LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey);
break;
}
{
this.LogDebug("Getting relays for {mode}", Mode);
LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey);
break;
}
}
this.LogDebug("Getting display with key {displayKey}", DisplayDeviceKey);
@@ -172,7 +210,8 @@ namespace PepperDash.Essentials.Devices.Common.Shades
this.LogDebug("Subscribing to {displayKey} feedbacks", DisplayDeviceKey);
DisplayDevice.IsWarmingUpFeedback.OutputChange += IsWarmingUpFeedback_OutputChange;
DisplayDevice.IsCoolingDownFeedback.OutputChange += IsCoolingDownFeedback_OutputChange;
DisplayDevice.IsCoolingDownFeedback.OutputChange +=
IsCoolingDownFeedback_OutputChange;
}
return base.CustomActivate();
@@ -183,10 +222,17 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary>
public void Raise()
{
if (RaiseRelay == null && LatchedRelay == null) return;
if (RaiseRelay == null && LatchedRelay == null)
return;
this.LogDebug("Raise called for {type}", Type);
if (Config.MuteOnScreenUp && DisplayDevice is IBasicVideoMuteWithFeedback videoMute)
{
this.LogInformation("Muting video because screen is going up");
videoMute.VideoMuteOn();
}
// If device is moving, bank the command
if (_isMoving)
{
@@ -200,33 +246,33 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs);
{
PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs);
// Set moving flag and start timer if movement time is configured
if (RaiseRelayConfig.MoveTimeInMs > 0)
{
_isMoving = true;
_currentMovement = RequestedState.Raise;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = RaiseRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = true;
}
break;
}
case eScreenLiftControlMode.latched:
// Set moving flag and start timer if movement time is configured
if (RaiseRelayConfig.MoveTimeInMs > 0)
{
LatchedRelay.Off();
InUpPosition = true;
break;
_isMoving = true;
_currentMovement = RequestedState.Raise;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = RaiseRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = true;
}
break;
}
case eScreenLiftControlMode.latched:
{
LatchedRelay.Off();
InUpPosition = true;
break;
}
}
}
@@ -235,7 +281,8 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary>
public void Lower()
{
if (LowerRelay == null && LatchedRelay == null) return;
if (LowerRelay == null && LatchedRelay == null)
return;
this.LogDebug("Lower called for {type}", Type);
@@ -252,33 +299,33 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs);
{
PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs);
// Set moving flag and start timer if movement time is configured
if (LowerRelayConfig.MoveTimeInMs > 0)
{
_isMoving = true;
_currentMovement = RequestedState.Lower;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = LowerRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = false;
}
break;
}
case eScreenLiftControlMode.latched:
// Set moving flag and start timer if movement time is configured
if (LowerRelayConfig.MoveTimeInMs > 0)
{
LatchedRelay.On();
InUpPosition = false;
break;
_isMoving = true;
_currentMovement = RequestedState.Lower;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = LowerRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = false;
}
break;
}
case eScreenLiftControlMode.latched:
{
LatchedRelay.On();
InUpPosition = false;
break;
}
}
}
@@ -339,16 +386,13 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{
output.On();
var timer = new Timer(pulseTime)
{
AutoReset = false
};
var timer = new Timer(pulseTime) { AutoReset = false };
timer.Elapsed += (sender, e) =>
{
output.Off();
timer.Dispose();
};
{
output.Off();
timer.Dispose();
};
timer.Start();
}
@@ -361,7 +405,10 @@ namespace PepperDash.Essentials.Devices.Common.Shades
}
else
{
this.LogWarning("Error: Unable to get relay device with key '{relayKey}'", relayKey);
this.LogWarning(
"Error: Unable to get relay device with key '{relayKey}'",
relayKey
);
return null;
}
}
@@ -375,11 +422,13 @@ namespace PepperDash.Essentials.Devices.Common.Shades
}
else
{
this.LogWarning("Error: Unable to get display device with key '{displayKey}'", displayKey);
this.LogWarning(
"Error: Unable to get display device with key '{displayKey}'",
displayKey
);
return null;
}
}
}
/// <summary>
@@ -387,7 +436,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary>
public class ScreenLiftControllerFactory : EssentialsDeviceFactory<RelayControlledShade>
{
/// <summary>
/// <summary>
/// Constructor for ScreenLiftControllerFactory
/// </summary>
public ScreenLiftControllerFactory()
@@ -404,4 +453,4 @@ namespace PepperDash.Essentials.Devices.Common.Shades
return new ScreenLiftController(dc.Key, dc.Name, props);
}
}
}
}

View File

@@ -5,37 +5,41 @@ using PepperDash.Essentials.Core.DeviceTypeInterfaces;
namespace PepperDash.Essentials.Devices.Common.Shades
{
/// <summary>
/// Represents a ScreenLiftControllerConfigProperties
/// </summary>
public class ScreenLiftControllerConfigProperties
{
/// <summary>
/// Gets or sets the DisplayDeviceKey
/// Represents a ScreenLiftControllerConfigProperties
/// </summary>
[JsonProperty("displayDeviceKey")]
public string DisplayDeviceKey { get; set; }
public class ScreenLiftControllerConfigProperties
{
/// <summary>
/// Gets or sets the DisplayDeviceKey
/// </summary>
[JsonProperty("displayDeviceKey")]
public string DisplayDeviceKey { get; set; }
/// <summary>
/// Gets or sets the Type
/// </summary>
[JsonProperty("type")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlType Type { get; set; }
/// <summary>
/// Gets or sets the Type
/// </summary>
[JsonProperty("type")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlType Type { get; set; }
/// <summary>
/// Gets or sets the Mode
/// </summary>
[JsonProperty("mode")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlMode Mode { get; set; }
/// <summary>
/// Gets or sets the Mode
/// </summary>
[JsonProperty("mode")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlMode Mode { get; set; }
/// <summary>
/// Gets or sets the Relays
/// </summary>
[JsonProperty("relays")]
public Dictionary<string, ScreenLiftRelaysConfig> Relays { get; set; }
/// <summary>
/// Gets or sets the Relays
/// </summary>
[JsonProperty("relays")]
public Dictionary<string, ScreenLiftRelaysConfig> Relays { get; set; }
}
}
/// <summary>
/// Mutes the display when the screen is in the up position
/// </summary>
[JsonProperty("muteOnScreenUp")]
public bool MuteOnScreenUp { get; set; }
}
}

View File

@@ -69,10 +69,11 @@ namespace PepperDash.Essentials.WebSocketServer
private readonly ConcurrentDictionary<string, string> pendingClientRegistrations = new ConcurrentDictionary<string, string>();
/// <summary>
/// Stores queues of pending client IDs per token for legacy clients (FIFO)
/// This ensures thread-safety when multiple legacy clients use the same token
/// Stores pending client registrations with timestamp for legacy clients
/// Key is token, Value is list of (clientId, timestamp) tuples
/// Most recent registration is used to handle duplicate join requests
/// </summary>
private readonly ConcurrentDictionary<string, ConcurrentQueue<string>> legacyClientIdQueues = new ConcurrentDictionary<string, ConcurrentQueue<string>>();
private readonly ConcurrentDictionary<string, ConcurrentBag<(string clientId, DateTime timestamp)>> legacyClientRegistrations = new ConcurrentDictionary<string, ConcurrentBag<(string, DateTime)>>();
/// <summary>
/// Gets the collection of UI clients
@@ -736,15 +737,23 @@ namespace PepperDash.Essentials.WebSocketServer
private UiClient BuildUiClient(string roomKey, JoinToken token, string key)
{
// Dequeue the next clientId for legacy client support (FIFO per token)
// Get the most recent unused clientId for this token (legacy support)
// New clients will override this ID in OnOpen with the validated query parameter value
var clientId = "pending";
if (legacyClientIdQueues.TryGetValue(key, out var queue) && queue.TryDequeue(out var dequeuedId))
if (legacyClientRegistrations.TryGetValue(key, out var registrations))
{
clientId = dequeuedId;
this.LogVerbose("Dequeued legacy clientId {clientId} for token {token}", clientId, key);
// Get most recent registration
var sorted = registrations.OrderByDescending(r => r.timestamp).ToList();
if (sorted.Any())
{
clientId = sorted.First().clientId;
// Remove it from the bag
var newBag = new ConcurrentBag<(string, DateTime)>(sorted.Skip(1));
legacyClientRegistrations.TryUpdate(key, newBag, registrations);
this.LogVerbose("Assigned most recent legacy clientId {clientId} for token {token}", clientId, key);
}
}
var c = new UiClient($"uiclient-{key}-{roomKey}-{clientId}", clientId, token.Token, token.TouchpanelKey);
this.LogInformation("Constructing UiClient with key {key} and temporary ID (will be set from query param)", key);
c.Controller = _parent;
@@ -753,8 +762,8 @@ namespace PepperDash.Essentials.WebSocketServer
c.Server = this; // Give UiClient access to server for ID registration
// Don't add to uiClients yet - will be added in OnOpen after ID is set from query param
c.ConnectionClosed += (o, a) =>
c.ConnectionClosed += (o, a) =>
{
uiClients.TryRemove(a.ClientId, out _);
// Clean up any pending registrations for this token
@@ -765,11 +774,11 @@ namespace PepperDash.Essentials.WebSocketServer
{
pendingClientRegistrations.TryRemove(k, out _);
}
// Clean up legacy queue if empty
if (legacyClientIdQueues.TryGetValue(key, out var legacyQueue) && legacyQueue.IsEmpty)
// Clean up legacy registrations if empty
if (legacyClientRegistrations.TryGetValue(key, out var legacyBag) && legacyBag.IsEmpty)
{
legacyClientIdQueues.TryRemove(key, out _);
legacyClientRegistrations.TryRemove(key, out _);
}
};
return c;
@@ -785,7 +794,7 @@ namespace PepperDash.Essentials.WebSocketServer
public bool RegisterUiClient(UiClient client, string clientId, string tokenKey)
{
var registrationKey = $"{tokenKey}-{clientId}";
// Verify this clientId was generated during a join request for this token
if (!pendingClientRegistrations.TryRemove(registrationKey, out _))
{
@@ -799,11 +808,63 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogWarning("Replacing existing client with duplicate id {id}", id);
return client;
});
this.LogInformation("Successfully registered UiClient with ID {clientId} for token {token}", clientId, tokenKey);
return true;
}
/// <summary>
/// Updates a client's ID when a mismatch is detected between stored ID and message ID
/// </summary>
/// <param name="oldClientId">The current/old client ID</param>
/// <param name="newClientId">The new client ID from the message</param>
/// <param name="tokenKey">The token key for validation</param>
/// <returns>True if update successful, false otherwise</returns>
public bool UpdateClientId(string oldClientId, string newClientId, string tokenKey)
{
if (string.IsNullOrEmpty(oldClientId) || string.IsNullOrEmpty(newClientId))
{
this.LogWarning("Cannot update client ID with null or empty values");
return false;
}
if (oldClientId == newClientId)
{
return true; // No update needed
}
// Verify the new clientId was registered for this token
var registrationKey = $"{tokenKey}-{newClientId}";
if (!pendingClientRegistrations.TryRemove(registrationKey, out _))
{
this.LogWarning("Cannot update to unregistered clientId {newClientId} for token {token}", newClientId, tokenKey);
return false;
}
// Get the existing client
if (!uiClients.TryRemove(oldClientId, out var client))
{
this.LogWarning("Cannot find client with old ID {oldClientId}", oldClientId);
return false;
}
// Update the client's ID
client.UpdateId(newClientId);
// Re-add with new ID
if (!uiClients.TryAdd(newClientId, client))
{
// If add fails, try to restore old entry
uiClients.TryAdd(oldClientId, client);
client.UpdateId(oldClientId);
this.LogError("Failed to update client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
return false;
}
this.LogInformation("Successfully updated client ID from {oldClientId} to {newClientId}", oldClientId, newClientId);
return true;
}
/// <summary>
/// Registers a UiClient using legacy flow (for backwards compatibility with older clients)
/// </summary>
@@ -821,7 +882,7 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogWarning("Replacing existing client with duplicate id {id} (legacy flow)", id);
return client;
});
this.LogInformation("Successfully registered UiClient with ID {clientId} using legacy flow", client.Id);
}
@@ -1133,16 +1194,17 @@ namespace PepperDash.Essentials.WebSocketServer
// Generate a client ID for this join request
var clientId = $"{Utilities.GetNextClientId()}";
var now = DateTime.UtcNow;
// Store in pending registrations for new clients that send clientId via query param
var registrationKey = $"{token}-{clientId}";
pendingClientRegistrations.TryAdd(registrationKey, clientId);
// Also enqueue for legacy clients (thread-safe FIFO per token)
var queue = legacyClientIdQueues.GetOrAdd(token, _ => new ConcurrentQueue<string>());
queue.Enqueue(clientId);
this.LogVerbose("Assigning ClientId: {clientId} for token: {token}", clientId, token);
// For legacy clients, store with timestamp instead of FIFO queue
var legacyBag = legacyClientRegistrations.GetOrAdd(token, _ => new ConcurrentBag<(string, DateTime)>());
legacyBag.Add((clientId, now));
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
// Construct WebSocket URL with clientId query parameter
var wsProtocol = "ws";

View File

@@ -26,6 +26,15 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Updates the client ID - only accessible from within the assembly (e.g., by the server)
/// </summary>
/// <param name="newId">The new client ID</param>
internal void UpdateId(string newId)
{
Id = newId;
}
/// <summary>
/// Token associated with this client
/// </summary>

View File

@@ -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>