From 8817d70f0793ee804747940da0f195e7cddff580 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 20 Mar 2026 12:03:00 -0600 Subject: [PATCH 01/21] feat: add interfaces for VLAN and PoE management in network switches --- .../INetworkSwitchControl.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs new file mode 100644 index 000000000..4ad6094dd --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs @@ -0,0 +1,48 @@ +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +{ + /// + /// Interface for network switches that support VLAN assignment on individual ports. + /// + public interface INetworkSwitchVlanManager + { + /// + /// Returns the current access VLAN ID configured on the port. + /// Return -1 when the value is unavailable (e.g. the switch has not been polled yet + /// or the implementation does not support VLAN queries). + /// + /// Switch port identifier + /// VLAN ID or -1 when unavailable + int GetPortCurrentVlan(string port); + + /// + /// Changes the access VLAN of a single switch port. + /// The implementation is responsible for entering/exiting privileged/config mode. + /// + /// Switch port identifier (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco) + /// Target VLAN ID (1-4093) + void SetPortVlan(string port, uint vlanId); + } + + /// + /// Interface for network switches that support Power over Ethernet (PoE) control on individual ports. + /// + public interface INetworkSwitchPoeManager + { + /// + /// Enables or disables PoE power delivery on a single switch port. + /// The implementation is responsible for entering/exiting privileged/config mode. + /// + /// Switch port identifier + /// True to enable PoE; false to disable PoE + void SetPortPoeState(string port, bool enabled); + } + + /// + /// Standardized interface for network switch devices that support per-port PoE control + /// and VLAN assignment. + /// + public interface INetworkSwitchPoeVlanManager : INetworkSwitchVlanManager, INetworkSwitchPoeManager + { + + } +} From 76311b83ff866fce29ad6768855ab8eb8dbabeba Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 20 Mar 2026 13:25:58 -0600 Subject: [PATCH 02/21] feat: add event and arguments for port state changes in network switch management --- .../INetworkSwitchControl.cs | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs index 4ad6094dd..6174ff282 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs @@ -1,3 +1,5 @@ +using System; + namespace PepperDash.Essentials.Core.DeviceTypeInterfaces { /// @@ -43,6 +45,58 @@ public interface INetworkSwitchPoeManager /// public interface INetworkSwitchPoeVlanManager : INetworkSwitchVlanManager, INetworkSwitchPoeManager { + /// + /// Event that is raised when the state of a switch port changes, such as a VLAN change or PoE state change. + /// + event EventHandler PortStateChanged; + + } + + /// + /// Event arguments for port state changes on a network switch, such as VLAN changes or PoE state changes. + /// + public class NetworkSwitchPortEventArgs : EventArgs + { + /// + /// The identifier of the port that changed state (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco). + /// + public string Port { get; private set; } + + /// + /// The type of event that occurred on the port (e.g. VLAN change, PoE enabled/disabled). + /// + public NetworkSwitchPortEventType EventType { get; private set; } + + /// + /// Constructor for NetworkSwitchPortEventArgs + /// + /// The identifier of the port that changed state + /// The type of event that occurred on the port + public NetworkSwitchPortEventArgs(string port, NetworkSwitchPortEventType eventType) + { + Port = port; + EventType = eventType; + } + } + + /// + /// Event arguments for port state changes on a network switch, such as VLAN changes or PoE state changes. + /// + public enum NetworkSwitchPortEventType + { + /// + /// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan + /// + VlanChanged, + /// + /// Indicates that the PoE state on a port has changed, either through a successful call to SetPortPoeState + /// + PoEDisabled, + + /// + /// Indicates that the PoE state on a port has changed, either through a successful call to SetPortPoeState + /// + PoEEnabled } } From 6db7581295d746040b0a84ffa058f1f9d53f0ca9 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 20 Mar 2026 14:48:58 -0600 Subject: [PATCH 03/21] feat: add Unknown event type to NetworkSwitchPortEventType enum for improved event handling --- .../DeviceTypeInterfaces/INetworkSwitchControl.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs index 6174ff282..0aaf89d29 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs @@ -84,6 +84,11 @@ public NetworkSwitchPortEventArgs(string port, NetworkSwitchPortEventType eventT /// public enum NetworkSwitchPortEventType { + /// + /// Indicates that the type of event is unknown or cannot be determined. + /// + Unknown, + /// /// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan /// From 84c730b7a1255c8c167d784f186ec68be4997cfe Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Fri, 20 Mar 2026 14:53:21 -0600 Subject: [PATCH 04/21] feat: enhance NetworkSwitchPortEventType enum with additional states for VLAN and PoE changes --- .../DeviceTypeInterfaces/INetworkSwitchControl.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs index 0aaf89d29..aca383330 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs @@ -89,16 +89,31 @@ public enum NetworkSwitchPortEventType /// Unknown, + /// + /// Indicates that a VLAN change is in progress on the port, either through a call to SetPortVlan or an external change detected by polling. + /// + VlanChangeInProgress, + /// /// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan /// VlanChanged, + /// + /// Indicates that PoE is being disabled on the port, either through a call to SetPortPoeState or an external change detected by polling. + /// + PoeDisableInProgress, + /// /// Indicates that the PoE state on a port has changed, either through a successful call to SetPortPoeState /// PoEDisabled, + /// + /// Indicates that PoE is being enabled on the port, either through a call to SetPortPoeState or an external change detected by polling. + /// + PoeEnableInProgress, + /// /// Indicates that the PoE state on a port has changed, either through a successful call to SetPortPoeState /// From 1b10910cc28324b98feef7ed6f15400b1fdc6632 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 1 Apr 2026 08:56:10 -0600 Subject: [PATCH 05/21] fix: update GetFeedbacksForDeviceRequestHandler to return JSON response for missing device --- .../GetFeedbacksForDeviceRequestHandler.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs index ca9eeb812..7d947e19f 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs @@ -1,13 +1,14 @@ -using System.Linq; +using System; +using System.Linq; using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; namespace PepperDash.Essentials.Core.Web.RequestHandlers { - /// - /// Represents a GetFeedbacksForDeviceRequestHandler - /// + /// + /// Represents a GetFeedbacksForDeviceRequestHandler + /// public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler { /// @@ -51,8 +52,20 @@ protected override void HandleGet(HttpCwsContext context) var device = DeviceManager.GetDeviceForKey(deviceObj.ToString()) as IHasFeedback; if (device == null) { - context.Response.StatusCode = 404; - context.Response.StatusDescription = "Not Found"; + context.Response.StatusCode = 200; + context.Response.StatusDescription = "OK"; + context.Response.ContentType = "application/json"; + context.Response.ContentEncoding = System.Text.Encoding.UTF8; + var resp = new + { + BoolValues = Array.Empty(), + IntValues = Array.Empty(), + SerialValues = Array.Empty() + }; + var respJs = JsonConvert.SerializeObject(resp, Formatting.Indented); + + context.Response.Write(respJs, false); + context.Response.End(); return; @@ -76,7 +89,7 @@ from feedback in device.Feedbacks.OfType() Value = feedback.IntValue }; - var stringFeedback = + var stringFeedback = from feedback in device.Feedbacks.OfType() where !string.IsNullOrEmpty(feedback.Key) select new From 6587444970842af8aba769121979dbe11441e6b0 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 21 May 2026 13:11:39 -0600 Subject: [PATCH 06/21] fix: update routing logic and enhance logging in Extensions and MockVC classes --- .../Routing/Extensions.cs | 20 ++++++++++++------- .../VideoCodec/MockVC/MockVC.cs | 2 +- .../VideoCodec/VideoCodecBase.cs | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index 631ebd994..be533b708 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -249,7 +249,7 @@ public static (RouteDescriptor, RouteDescriptor) GetRouteToSource(this IRoutingI } // otherwise, audioVideo needs to be handled as two steps. - Debug.LogDebug(destination, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", source.Key, signalType); + Debug.LogDebug(destination, "Attempting to build source route from {destinationKey} to {sourceKey} of type {type}", destination.Key, source.Key, signalType); RouteDescriptor audioRouteDescriptor; @@ -374,24 +374,28 @@ public static void MapDestinationsToSources() IndexTieLines(); } - var sinks = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); - var sources = DeviceManager.AllDevices.OfType().Where(d => !(d is IRoutingInputsOutputs)); + var sinks = DeviceManager.AllDevices.OfType(); + var sources = DeviceManager.AllDevices.OfType(); - foreach (var sink in sinks) + foreach (var sink in sinks.Where(d => !(d is IRoutingInputsOutputs))) { - foreach (var source in sources) + foreach (var source in sources.Where(d => !(d is IRoutingInputsOutputs))) { foreach (var inputPort in sink.InputPorts) { foreach (var outputPort in source.OutputPorts) { - var (audioOrSingleRoute, videoRoute) = sink.GetRouteToSource(source, inputPort.Type, inputPort, outputPort); + var (audioOrSingleRoute, videoRoute) = sink.GetRouteToSource(source, outputPort.Type, inputPort, outputPort); if (audioOrSingleRoute == null && videoRoute == null) { continue; } + Debug.LogVerbose("AudioOrSingleRoute Found: {audioRoute}", audioOrSingleRoute); + + Debug.LogVerbose("VideoRoute Found: {videoRoute}", videoRoute); + if (audioOrSingleRoute != null) { // Only add routes that have actual switching steps @@ -646,6 +650,8 @@ private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOu // Only the ones that are routing devices var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs); + Debug.LogVerbose(destination, "Found {tieLineCount} tie lines to walk for {destinationKey}", midpointTieLines.Count(), destination.Key); + //Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration if (alreadyCheckedDevices == null) alreadyCheckedDevices = new List(); @@ -685,7 +691,7 @@ private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOu if (goodInputPort == null) { - Debug.LogVerbose(destination, "No route found to {0}", source.Key); + Debug.LogVerbose(destination, "No route found to {0} from destination {1} for type {2}", source.Key, destination.Key, signalType); // Cache this as an impossible route _impossibleRoutes.TryAdd(routeKey, 0); diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs index e9f7908ce..bdefa09dc 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/MockVC/MockVC.cs @@ -21,7 +21,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// /// Represents a MockVC /// - public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets + public class MockVC : VideoCodecBase, IRoutingSource, IHasCallHistory, IHasScheduleAwareness, IHasCallFavorites, IHasDirectory, IHasCodecCameras, IHasCameraAutoMode, IHasCodecRoomPresets, IRoutingInputs { /// /// Gets or sets the PropertiesConfig diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs index b0b5ef55b..04c7e64d9 100644 --- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs +++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs @@ -26,7 +26,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec /// /// Base class for video codec devices /// - public abstract class VideoCodecBase : ReconfigurableDevice, IRoutingInputsOutputs, + public abstract class VideoCodecBase : ReconfigurableDevice, IUsageTracking, IHasDialer, IHasContentSharing, ICodecAudio, iVideoCodecInfo, IBridgeAdvanced, IHasStandbyMode { private const int XSigEncoding = 28591; From 92d13f29a9dfac9e9a919f152fc6ce6cd3b47584 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 21 May 2026 16:43:00 -0600 Subject: [PATCH 07/21] fix: refine routing logic by filtering IRoutingInputs and IRoutingOutputs, enhance logging for route mapping --- .../Routing/Extensions.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index be533b708..78d0732ee 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -374,12 +374,14 @@ public static void MapDestinationsToSources() IndexTieLines(); } - var sinks = DeviceManager.AllDevices.OfType(); - var sources = DeviceManager.AllDevices.OfType(); + var sinks = DeviceManager.AllDevices.OfType() + .Where(d => !(d is IRoutingInputsOutputs)).ToList(); + var sources = DeviceManager.AllDevices.OfType() + .Where(d => !(d is IRoutingInputsOutputs)).ToList(); - foreach (var sink in sinks.Where(d => !(d is IRoutingInputsOutputs))) + foreach (var sink in sinks) { - foreach (var source in sources.Where(d => !(d is IRoutingInputsOutputs))) + foreach (var source in sources) { foreach (var inputPort in sink.InputPorts) { @@ -392,10 +394,6 @@ public static void MapDestinationsToSources() continue; } - Debug.LogVerbose("AudioOrSingleRoute Found: {audioRoute}", audioOrSingleRoute); - - Debug.LogVerbose("VideoRoute Found: {videoRoute}", videoRoute); - if (audioOrSingleRoute != null) { // Only add routes that have actual switching steps @@ -404,6 +402,10 @@ public static void MapDestinationsToSources() continue; } + Debug.LogVerbose("Route mapped: {source} -> {sink} via {input}/{output}, type {type}", + source.Key, sink.Key, + inputPort.Key, outputPort.Key, audioOrSingleRoute.SignalType); + // 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)) @@ -435,6 +437,10 @@ public static void MapDestinationsToSources() continue; } + Debug.LogVerbose("Video route mapped: {source} -> {sink} via {input}/{output}", + source.Key, sink.Key, + inputPort.Key, outputPort.Key); + RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute); } } @@ -648,10 +654,12 @@ private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOu // No direct tie? Run back out on the inputs' attached devices... // Only the ones that are routing devices - var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs); - - Debug.LogVerbose(destination, "Found {tieLineCount} tie lines to walk for {destinationKey}", midpointTieLines.Count(), destination.Key); + var midpointTieLines = destinationTieLines + .Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs) + .ToList(); + Debug.LogVerbose(destination, "Found {tieLineCount} tie lines to walk for {destinationKey}", midpointTieLines.Count, destination.Key); + //Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration if (alreadyCheckedDevices == null) alreadyCheckedDevices = new List(); From 74a12c9671e4dac87b4eecef6ce8abb7e12f5185 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Thu, 21 May 2026 18:47:25 -0600 Subject: [PATCH 08/21] fix: add null or empty checks for midpoint keys in RoutingFeedbackManager --- .../Routing/RoutingFeedbackManager.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs index 16822ff2f..7fe41c5c7 100644 --- a/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs +++ b/src/PepperDash.Essentials.Core/Routing/RoutingFeedbackManager.cs @@ -66,6 +66,9 @@ private void BuildMidpointSinkMap() foreach (var midpointKey in upstreamMidpoints) { + if (string.IsNullOrEmpty(midpointKey)) + continue; + if (!midpointToSinksMap.ContainsKey(midpointKey)) midpointToSinksMap[midpointKey] = new HashSet(); @@ -115,7 +118,8 @@ private void TraceUpstreamMidpoints(TieLine tieLine, HashSet midpoints, if (tieLine.SourcePort.ParentDevice is IRoutingWithFeedback midpoint) { - midpoints.Add(midpoint.Key); + if (!string.IsNullOrEmpty(midpoint.Key)) + midpoints.Add(midpoint.Key); // Find upstream TieLines connected to this midpoint's inputs var midpointInputs = (midpoint as IRoutingInputs)?.InputPorts; @@ -244,6 +248,9 @@ private void RebuildMapForSink(IRoutingSinkWithSwitchingWithInputPort sink) var upstreamMidpoints = GetUpstreamMidpoints(sink); foreach (var midpointKey in upstreamMidpoints) { + if (string.IsNullOrEmpty(midpointKey)) + continue; + if (!midpointToSinksMap.ContainsKey(midpointKey)) midpointToSinksMap[midpointKey] = new HashSet(); From b32bab0d335b75a56de154c4352086e1f20db7c3 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 9 Jun 2026 09:32:59 -0500 Subject: [PATCH 09/21] fix: remove impossibleRoutes cache The cache wasn't being cleared correctly, and was an unnecessary add. --- .../Routing/Extensions.cs | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index 78d0732ee..5063f8b30 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -52,13 +51,6 @@ public static class Extensions /// private static Dictionary> _tieLinesBySource; - /// - /// Cache of failed route attempts to avoid re-checking impossible paths. - /// Format: "sourceKey|destKey|signalType" - /// Uses ConcurrentDictionary as a thread-safe set (byte value is unused). - /// - private static readonly ConcurrentDictionary _impossibleRoutes = new ConcurrentDictionary(); - /// /// Indexes all TieLines by source and destination device keys for faster lookups. /// Should be called once at system startup after all TieLines are created. @@ -121,29 +113,6 @@ private static IEnumerable GetTieLinesForSource(string sourceKey) return TieLineCollection.Default.Where(t => t.SourcePort.ParentDevice.Key == sourceKey); } - /// - /// Creates a cache key for route impossibility tracking. - /// - /// Source device key - /// Destination device key - /// Source port key - /// Destination port key - /// Signal type - /// Cache key string - private static string GetRouteKey(string sourceKey, string destKey, string sourcePortKey, string destinationPortKey, eRoutingSignalType type) - { - return $"{sourceKey}|{destKey}|{sourcePortKey}|{destinationPortKey}|{type}"; - } - - /// - /// Clears the impossible routes cache. Should be called if TieLines are added/removed at runtime. - /// - public static void ClearImpossibleRoutesCache() - { - _impossibleRoutes.Clear(); - Debug.LogInformation("Impossible routes cache cleared"); - } - /// /// Gets any existing RouteDescriptor for a destination, clears it using ReleaseRoute /// and then attempts a new Route and if sucessful, stores that RouteDescriptor @@ -594,14 +563,6 @@ private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOu { cycle++; - // Check if this route has already been determined to be impossible - var routeKey = GetRouteKey(source.Key, destination.Key, sourcePort?.Key ?? "auto", destinationPort?.Key ?? "auto", signalType); - if (_impossibleRoutes.ContainsKey(routeKey)) - { - Debug.LogVerbose("Route {0} is cached as impossible, skipping", routeKey); - return false; - } - Debug.LogVerbose("GetRouteToSource: {cycle} {sourceKey}:{sourcePortKey}--> {destinationKey}:{destinationPortKey} {type}", null, cycle, source.Key, sourcePort?.Key ?? "auto", destination.Key, destinationPort?.Key ?? "auto", signalType.ToString()); RoutingInputPort goodInputPort = null; @@ -701,9 +662,6 @@ private static bool GetRouteToSource(this IRoutingInputs destination, IRoutingOu { Debug.LogVerbose(destination, "No route found to {0} from destination {1} for type {2}", source.Key, destination.Key, signalType); - // Cache this as an impossible route - _impossibleRoutes.TryAdd(routeKey, 0); - return false; } From 162a06f9e90aedcb6bdda10a38b81b48ccb3009c Mon Sep 17 00:00:00 2001 From: aknous Date: Wed, 10 Jun 2026 21:57:55 -0400 Subject: [PATCH 10/21] feat: adds interface for wireless sharing --- .../IHasWirelessSharing.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs new file mode 100644 index 000000000..6cd3835e6 --- /dev/null +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasWirelessSharing.cs @@ -0,0 +1,48 @@ +using System; + +namespace PepperDash.Essentials.Core.DeviceTypeInterfaces +{ + /// + /// Defines the contract for a wireless presentation endpoint that reports whether a wireless + /// sharing session is currently active. Implemented by platforms such as Crestron AirMedia, + /// Mersive Solstice, Barco ClickShare, Miracast/Teams receivers, etc. Allows consumers (e.g. + /// room plugins) to react to wireless sharing activity without taking a dependency on any + /// concrete device implementation. + /// + /// + /// This refers specifically to wireless screen/device mirroring, as distinct from in-call + /// content sharing on a video conference. + /// + public interface IHasWirelessSharing + { + /// + /// Reports whether a wireless sharing session is currently active (content is being presented). + /// + BoolFeedback IsSharingFeedback { get; } + + /// + /// Raised when wireless sharing starts or stops. The event args carry the new sharing state. + /// + event EventHandler SharingChanged; + } + + /// + /// Event arguments describing a change in wireless sharing state. + /// + public class WirelessSharingEventArgs : EventArgs + { + /// + /// True if a wireless sharing session is active (content is being presented), false otherwise. + /// + public bool IsSharing { get; private set; } + + /// + /// Creates a new . + /// + /// True if a wireless sharing session is active, false otherwise. + public WirelessSharingEventArgs(bool isSharing) + { + IsSharing = isSharing; + } + } +} From d18fca8a9889a99d5d59417c4e33d48b4b559865 Mon Sep 17 00:00:00 2001 From: Neil Dorin Date: Wed, 10 Jun 2026 13:34:05 -0600 Subject: [PATCH 11/21] fix: handle null properties in DeviceMessageBase and DeviceStateMessageBase --- .../Messengers/DeviceMessageBase.cs | 8 ++++---- .../Messengers/DeviceStateMessageBase.cs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs index 54a6ec361..0198df2f5 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs @@ -10,7 +10,7 @@ public abstract class DeviceMessageBase /// /// The device key /// - [JsonProperty("key")] + [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] /// /// Gets or sets the Key /// @@ -19,19 +19,19 @@ public abstract class DeviceMessageBase /// /// The device name /// - [JsonProperty("name")] + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] public string Name { get; set; } /// /// The type of the message class /// - [JsonProperty("messageType")] + [JsonProperty("messageType", NullValueHandling = NullValueHandling.Ignore)] public string MessageType => GetType().Name; /// /// Gets or sets the MessageBasePath /// - [JsonProperty("messageBasePath")] + [JsonProperty("messageBasePath", NullValueHandling = NullValueHandling.Ignore)] public string MessageBasePath { get; set; } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs index a5df51a84..4241b69cd 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs @@ -12,7 +12,8 @@ public class DeviceStateMessageBase : DeviceMessageBase /// /// The interfaces implmented by the device sending the messsage /// - [JsonProperty("interfaces")] + [JsonProperty("interfaces", NullValueHandling = NullValueHandling.Ignore)] + [Obsolete("Interfaces is no longer supported and will be removed in a future release. Interfaces for all devices are now retrieved via the /joinroom endpoint in the MobileControlWebsocketServer")] public List Interfaces { get; private set; } /// From a7b839296e5de20293c842ea08d52a36674315db Mon Sep 17 00:00:00 2001 From: equinoy <153123103+equinoy@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:05:14 -0500 Subject: [PATCH 12/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../DeviceTypeInterfaces/INetworkSwitchControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs index aca383330..077e20381 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs @@ -115,7 +115,7 @@ public enum NetworkSwitchPortEventType PoeEnableInProgress, /// - /// Indicates that the PoE state on a port has changed, either through a successful call to SetPortPoeState + /// Indicates that PoE has been enabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling. /// PoEEnabled } From e03e45ccf5c0fd3c4510766fb14cf5691808c929 Mon Sep 17 00:00:00 2001 From: equinoy <153123103+equinoy@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:05:25 -0500 Subject: [PATCH 13/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../DeviceTypeInterfaces/INetworkSwitchControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs index 077e20381..5e1e4f1a9 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs @@ -80,7 +80,7 @@ public NetworkSwitchPortEventArgs(string port, NetworkSwitchPortEventType eventT } /// - /// Event arguments for port state changes on a network switch, such as VLAN changes or PoE state changes. + /// Enumeration of network switch port state change event types (e.g. VLAN changes or PoE state changes). /// public enum NetworkSwitchPortEventType { From b982219e2f0ef1bfab98a42fa6f934acfd9c247a Mon Sep 17 00:00:00 2001 From: equinoy <153123103+equinoy@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:05:36 -0500 Subject: [PATCH 14/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../DeviceTypeInterfaces/INetworkSwitchControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs index 5e1e4f1a9..37fb024de 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs @@ -95,7 +95,7 @@ public enum NetworkSwitchPortEventType VlanChangeInProgress, /// - /// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan + /// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan or an external change detected by polling. /// VlanChanged, From a22047410158b065a42eaf858b9345ade92179ee Mon Sep 17 00:00:00 2001 From: equinoy <153123103+equinoy@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:05:50 -0500 Subject: [PATCH 15/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../DeviceTypeInterfaces/INetworkSwitchControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs index 37fb024de..6c6e5598a 100644 --- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs +++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs @@ -105,7 +105,7 @@ public enum NetworkSwitchPortEventType PoeDisableInProgress, /// - /// Indicates that the PoE state on a port has changed, either through a successful call to SetPortPoeState + /// Indicates that PoE has been disabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling. /// PoEDisabled, From 8d3edde28ca6a50e7dc0551af2c41a5a72c9d11b Mon Sep 17 00:00:00 2001 From: aknous Date: Wed, 17 Jun 2026 10:06:00 -0400 Subject: [PATCH 16/21] feat: adds config props to disable power on/off automation --- .../Displays/ScreenLiftController.cs | 18 ++++++++++++++++++ .../ScreenLiftControllerConfigProperties.cs | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs index 80c1f8ee6..d542f52a0 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs @@ -152,6 +152,15 @@ ScreenLiftControllerConfigProperties config private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e) { + if (Config.DisableAutoRaiseOnPowerOff) + { + this.LogDebug( + "Auto-raise on power-off disabled for {type}; leaving position unchanged (manual control only)", + Type + ); + return; + } + if ( !DisplayDevice.IsCoolingDownFeedback.BoolValue && Type == eScreenLiftControlType.lift @@ -174,6 +183,15 @@ private void IsWarmingUpFeedback_OutputChange(object sender, FeedbackEventArgs e { if (DisplayDevice.IsWarmingUpFeedback.BoolValue) { + if (Config.DisableAutoLowerOnPowerOn) + { + this.LogDebug( + "Auto-lower on power-on disabled for {type}; leaving position unchanged (manual control only)", + Type + ); + return; + } + Lower(); } } diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs index 1c4f9906a..88113a08c 100644 --- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs +++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs @@ -41,5 +41,23 @@ public class ScreenLiftControllerConfigProperties /// [JsonProperty("muteOnScreenUp")] public bool MuteOnScreenUp { get; set; } + + /// + /// When true, this controller does NOT automatically lower when its assigned display powers on + /// (warms up). Manual Raise/Lower still work, and the power-off auto-raise is unaffected. Intended + /// for a projector screen that must not auto-drop in a public space for safety, while the projector + /// lift (a separate controller) can still drop automatically. + /// + [JsonProperty("disableAutoLowerOnPowerOn")] + public bool DisableAutoLowerOnPowerOn { get; set; } + + /// + /// When true, this controller does NOT automatically raise when its assigned display powers off + /// (cools down). Manual Raise/Lower still work, and the power-on auto-lower is unaffected. The + /// companion to ; together they make a controller fully + /// manual while leaving other controllers (e.g. the lift) on their default automatic behavior. + /// + [JsonProperty("disableAutoRaiseOnPowerOff")] + public bool DisableAutoRaiseOnPowerOff { get; set; } } } From aa76551500380bd705e3dbe13c977e3e962a8556 Mon Sep 17 00:00:00 2001 From: Erik Meyer Date: Thu, 16 Jul 2026 08:40:33 -0400 Subject: [PATCH 17/21] fix: guard CecPortController against null StreamCec and lazily subscribe --- .../Comm and IR/CecPortController.cs | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs index 298bbc9b3..4b381dd0a 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs @@ -39,6 +39,8 @@ public class CecPortController : Device, IBasicCommunicationWithStreamDebugging ICec Port; + bool _cecSubscribed; + /// /// Constructor /// @@ -54,7 +56,7 @@ public CecPortController(string key, Func + /// Subscribes to the CEC change event once is available. + /// Safe to call repeatedly; the subscription is only wired a single time. This allows the + /// receive path to self-heal if StreamCec was null at construction (e.g. the underlying + /// device had not yet come online). + /// + void TryEnsureCecSubscription() + { + if (_cecSubscribed) + return; + + if (Port?.StreamCec == null) + { + Debug.LogMessage(LogEventLevel.Warning, this, "StreamCec is not available; CEC feedback is deferred until the device is ready"); + return; + } + Port.StreamCec.CecChange += new CecChangeEventHandler(StreamCec_CecChange); + _cecSubscribed = true; + Debug.LogMessage(LogEventLevel.Information, this, "Subscribed to CEC feedback"); } void StreamCec_CecChange(Cec cecDevice, CecEventArgs args) @@ -104,8 +128,9 @@ void OnDataReceived(string s) /// public void SendText(string text) { - if (Port == null) + if (Port?.StreamCec == null) return; + TryEnsureCecSubscription(); this.PrintSentText(text); Port.StreamCec.Send.StringValue = text; } @@ -115,8 +140,9 @@ public void SendText(string text) /// public void SendBytes(byte[] bytes) { - if (Port == null) + if (Port?.StreamCec == null) return; + TryEnsureCecSubscription(); var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); this.PrintSentBytes(bytes); Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); From f4cb17314d4bc8a82f0eb6b58a16779d736d4d31 Mon Sep 17 00:00:00 2001 From: erikdred <88980320+erikdred@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:21:45 -0400 Subject: [PATCH 18/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../GetFeedbacksForDeviceRequestHandler.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs index 7d947e19f..9831fa911 100644 --- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs +++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs @@ -52,20 +52,8 @@ protected override void HandleGet(HttpCwsContext context) var device = DeviceManager.GetDeviceForKey(deviceObj.ToString()) as IHasFeedback; if (device == null) { - context.Response.StatusCode = 200; - context.Response.StatusDescription = "OK"; - context.Response.ContentType = "application/json"; - context.Response.ContentEncoding = System.Text.Encoding.UTF8; - var resp = new - { - BoolValues = Array.Empty(), - IntValues = Array.Empty(), - SerialValues = Array.Empty() - }; - var respJs = JsonConvert.SerializeObject(resp, Formatting.Indented); - - context.Response.Write(respJs, false); - + context.Response.StatusCode = 404; + context.Response.StatusDescription = "Not Found"; context.Response.End(); return; From fbf4c56403e9ec7ac51e0428ece3c4ad42201435 Mon Sep 17 00:00:00 2001 From: erikdred <88980320+erikdred@users.noreply.github.com> Date: Thu, 16 Jul 2026 09:23:18 -0400 Subject: [PATCH 19/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Messengers/DeviceStateMessageBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs index 4241b69cd..c5c0ab655 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs @@ -13,7 +13,7 @@ public class DeviceStateMessageBase : DeviceMessageBase /// The interfaces implmented by the device sending the messsage /// [JsonProperty("interfaces", NullValueHandling = NullValueHandling.Ignore)] - [Obsolete("Interfaces is no longer supported and will be removed in a future release. Interfaces for all devices are now retrieved via the /joinroom endpoint in the MobileControlWebsocketServer")] + [Obsolete("Interfaces are no longer supported and will be removed in a future release. Interfaces for all devices are now retrieved via the /joinroom endpoint in the MobileControlWebsocketServer")] public List Interfaces { get; private set; } /// From 36c15b8d959cd17c61399fd3fdd0190641a724cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:25:25 +0000 Subject: [PATCH 20/21] Clarify CecPortController subscription XML comment --- .../Comm and IR/CecPortController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs index 4b381dd0a..a5aa0d131 100644 --- a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs +++ b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs @@ -75,9 +75,9 @@ public CecPortController(string key, ICec port) /// /// Subscribes to the CEC change event once is available. - /// Safe to call repeatedly; the subscription is only wired a single time. This allows the - /// receive path to self-heal if StreamCec was null at construction (e.g. the underlying - /// device had not yet come online). + /// Safe to call repeatedly; the subscription is only wired a single time. + /// If StreamCec is null during construction, this is retried when send methods invoke + /// this method later. /// void TryEnsureCecSubscription() { From 14cd08207621ee4666c6467e07ebba485fbe1817 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:26:24 +0000 Subject: [PATCH 21/21] Remove redundant IRoutingInputsOutputs filtering in route mapping loops --- src/PepperDash.Essentials.Core/Routing/Extensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs index 2b5b3c8c9..303a01942 100644 --- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs +++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs @@ -348,9 +348,9 @@ public static void MapDestinationsToSources() var sources = DeviceManager.AllDevices.OfType() .Where(d => !(d is IRoutingInputsOutputs)).ToList(); - foreach (var sink in sinks.Where(d => !(d is IRoutingInputsOutputs))) + foreach (var sink in sinks) { - foreach (var source in sources.Where(d => !(d is IRoutingInputsOutputs))) + foreach (var source in sources) { foreach (var inputPort in sink.InputPorts) {