diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs
index 298bbc9b3..a5aa0d131 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.
+ /// If StreamCec is null during construction, this is retried when send methods invoke
+ /// this method later.
+ ///
+ 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));
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;
+ }
+ }
+}
diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs
new file mode 100644
index 000000000..6c6e5598a
--- /dev/null
+++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/INetworkSwitchControl.cs
@@ -0,0 +1,122 @@
+using System;
+
+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
+ {
+ ///
+ /// 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;
+ }
+ }
+
+ ///
+ /// Enumeration of network switch port state change event types (e.g. VLAN changes or PoE state changes).
+ ///
+ public enum NetworkSwitchPortEventType
+ {
+ ///
+ /// Indicates that the type of event is unknown or cannot be determined.
+ ///
+ 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 or an external change detected by polling.
+ ///
+ 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 PoE has been disabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling.
+ ///
+ 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 PoE has been enabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling.
+ ///
+ PoEEnabled
+ }
+}
diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs
index be533b708..303a01942 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
@@ -374,12 +343,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)
{
@@ -404,6 +375,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 +410,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);
}
}
@@ -588,14 +567,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;
@@ -648,10 +619,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();
@@ -693,9 +666,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;
}
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs
index ca9eeb812..9831fa911 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
{
///
@@ -76,7 +77,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
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; }
}
}
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..c5c0ab655 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 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; }
///