diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index ab9f2732..235fee48 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -1,6 +1,6 @@
- 2.36.6-local
+ 2.42.1-local
$(Version)
PepperDash Technology
PepperDash Technology
diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs
index df44ab51..546a2a67 100644
--- a/src/PepperDash.Core/Comm/GenericSshClient.cs
+++ b/src/PepperDash.Core/Comm/GenericSshClient.cs
@@ -151,6 +151,8 @@ namespace PepperDash.Core
// Thread-safety lock for state changes
private readonly object _stateLock = new object();
+ private volatile bool _isProgramStopping;
+
private bool disconnectLogged = false;
///
@@ -207,11 +209,9 @@ namespace PepperDash.Core
{
if (programEventType == eProgramStatusEventType.Stopping)
{
- if (client != null)
- {
- this.LogDebug("Program stopping. Closing connection");
- Disconnect();
- }
+ _isProgramStopping = true;
+ this.LogDebug("Program stopping. Closing connection");
+ Disconnect();
}
}
@@ -228,6 +228,12 @@ namespace PepperDash.Core
return;
}
+ if (_isProgramStopping)
+ {
+ this.LogDebug("Skipping connect because program is stopping");
+ return;
+ }
+
ConnectEnabled = true;
try
@@ -287,13 +293,7 @@ namespace PepperDash.Core
}
catch (SshConnectionException e)
{
- var ie = e.InnerException; // The details are inside!!
-
- if (ie is SocketException)
- {
- this.LogError("CONNECTION failure: Cannot reach host");
- this.LogVerbose(ie, "Exception details: ");
- }
+ var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all
if (ie is System.Net.Sockets.SocketException socketException)
{
@@ -301,20 +301,20 @@ namespace PepperDash.Core
Hostname, Port);
this.LogVerbose(socketException, "SocketException details: ");
}
- if (ie is SshAuthenticationException)
+ else if (ie is SshAuthenticationException)
{
this.LogError("Authentication failure for username {userName}", Username);
this.LogVerbose(ie, "AuthenticationException details: ");
}
else
{
- this.LogError("Error on connect: {error}", ie.Message);
- this.LogVerbose(ie, "Exception details: ");
+ this.LogError("Error on connect: {error}", ie?.Message ?? e.Message);
+ this.LogVerbose(ie ?? e, "Exception details: ");
}
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
- if (AutoReconnect)
+ if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer();
@@ -326,7 +326,7 @@ namespace PepperDash.Core
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
- if (AutoReconnect)
+ if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer();
@@ -338,7 +338,7 @@ namespace PepperDash.Core
this.LogVerbose(e, "Exception details: ");
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
- if (AutoReconnect)
+ if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer();
@@ -473,7 +473,7 @@ namespace PepperDash.Core
{
connectLock.Release();
}
- if (AutoReconnect && ConnectEnabled)
+ if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer();
@@ -516,7 +516,10 @@ namespace PepperDash.Core
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
- StartReconnectTimer();
+ if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
+ {
+ StartReconnectTimer();
+ }
}
catch (Exception ex)
{
@@ -549,7 +552,10 @@ namespace PepperDash.Core
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
- StartReconnectTimer();
+ if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
+ {
+ StartReconnectTimer();
+ }
}
catch (Exception ex)
{
diff --git a/src/PepperDash.Core/PepperDash.Core.csproj b/src/PepperDash.Core/PepperDash.Core.csproj
index daa5c6da..747aefb6 100644
--- a/src/PepperDash.Core/PepperDash.Core.csproj
+++ b/src/PepperDash.Core/PepperDash.Core.csproj
@@ -43,7 +43,7 @@
-
+
diff --git a/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs b/src/PepperDash.Essentials.Core/Comm and IR/CecPortController.cs
index 298bbc9b..a5aa0d13 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 @@ namespace PepperDash.Essentials.Core
ICec Port;
+ bool _cecSubscribed;
+
///
/// Constructor
///
@@ -54,7 +56,7 @@ namespace PepperDash.Essentials.Core
{
Port = postActivationFunc(config);
- Port.StreamCec.CecChange += StreamCec_CecChange;
+ TryEnsureCecSubscription();
});
}
@@ -68,7 +70,29 @@ namespace PepperDash.Essentials.Core
{
Port = port;
+ TryEnsureCecSubscription();
+ }
+
+ ///
+ /// 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 @@ namespace PepperDash.Essentials.Core
///
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 @@ namespace PepperDash.Essentials.Core
///
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 00000000..6cd3835e
--- /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 00000000..6c6e5598
--- /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/Devices/IHasDspPresetSave.cs b/src/PepperDash.Essentials.Core/Devices/IHasDspPresetSave.cs
new file mode 100644
index 00000000..c628129e
--- /dev/null
+++ b/src/PepperDash.Essentials.Core/Devices/IHasDspPresetSave.cs
@@ -0,0 +1,14 @@
+namespace PepperDash.Essentials.Core
+{
+ ///
+ /// Defines the contract for IHasDspPresetSave
+ ///
+ public interface IHasDspPresetSave : IDspPresets
+ {
+ ///
+ /// Saves the preset by key
+ ///
+ /// key of preset to save
+ void SavePreset(string key);
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj
index 251ba316..b7f333bd 100644
--- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj
+++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj
@@ -25,7 +25,7 @@
bin\$(Configuration)\PepperDash_Essentials_Core.xml
-
+
diff --git a/src/PepperDash.Essentials.Core/Routing/Extensions.cs b/src/PepperDash.Essentials.Core/Routing/Extensions.cs
index 9b9c458e..303a0194 100644
--- a/src/PepperDash.Essentials.Core/Routing/Extensions.cs
+++ b/src/PepperDash.Essentials.Core/Routing/Extensions.cs
@@ -343,12 +343,14 @@ namespace PepperDash.Essentials.Core
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)
{
@@ -373,6 +375,10 @@ namespace PepperDash.Essentials.Core
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))
@@ -404,6 +410,10 @@ namespace PepperDash.Essentials.Core
continue;
}
+ Debug.LogVerbose("Video route mapped: {source} -> {sink} via {input}/{output}",
+ source.Key, sink.Key,
+ inputPort.Key, outputPort.Key);
+
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute);
}
}
@@ -609,10 +619,12 @@ namespace PepperDash.Essentials.Core
// 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();
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/GetFeedbacksForDeviceRequestHandler.cs
index ca9eeb81..9831fa91 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 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
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 80c1f8ee..d542f52a 100644
--- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs
+++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs
@@ -152,6 +152,15 @@ namespace PepperDash.Essentials.Devices.Common.Shades
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 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{
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 1c4f9906..88113a08 100644
--- a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs
+++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftControllerConfigProperties.cs
@@ -41,5 +41,23 @@ namespace PepperDash.Essentials.Devices.Common.Shades
///
[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.Devices.Common/PepperDash.Essentials.Devices.Common.csproj b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj
index 7be4372d..317fa2d3 100644
--- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj
+++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj
@@ -29,6 +29,6 @@
-
+
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs
index 54a6ec36..0198df2f 100644
--- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceMessageBase.cs
@@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
///
/// The device key
///
- [JsonProperty("key")]
+ [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
///
/// Gets or sets the Key
///
@@ -19,19 +19,19 @@ namespace PepperDash.Essentials.AppServer.Messengers
///
/// 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 a5df51a8..c5c0ab65 100644
--- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceStateMessageBase.cs
@@ -12,7 +12,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
///
/// 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; }
///
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj
index d13d1a09..a9223061 100644
--- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj
@@ -33,7 +33,7 @@
-
+
diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj
index 235e0899..b0e2dd9f 100644
--- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj
+++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj
@@ -38,7 +38,7 @@
-
+
diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs
index 5830782d..262cb34e 100644
--- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs
+++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs
@@ -25,11 +25,28 @@ namespace PepperDash.Essentials.Touchpanel
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
/// and mobile control functionality for Crestron touchpanels.
///
- public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme
+ public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme, ICommunicationMonitor
{
private readonly MobileControlTouchpanelProperties localConfig;
private IMobileControlRoomMessenger _bridge;
+ ///
+ /// Gets the CommunicationMonitor tracking the panel's online/offline state
+ ///
+ public StatusMonitorBase CommunicationMonitor { get; private set; }
+
+ private sealed class NullCommunicationMonitor : StatusMonitorBase
+ {
+ public NullCommunicationMonitor(IKeyed parent) : base(parent, 120000, 300000)
+ {
+ Status = MonitorStatus.InError;
+ Message = "Panel is not initialized";
+ }
+
+ public override void Start() { }
+ public override void Stop() { }
+ }
+
private string _appUrl;
///
@@ -128,6 +145,11 @@ namespace PepperDash.Essentials.Touchpanel
{
localConfig = config;
+ if (panel != null)
+ {
+ CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, panel, 120000, 300000);
+ }
+
AddPostActivationAction(SubscribeForMobileControlUpdates);
ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme);
@@ -366,6 +388,8 @@ namespace PepperDash.Essentials.Touchpanel
///
public override bool CustomActivate()
{
+ CommunicationMonitor?.Start();
+
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this);
@@ -393,6 +417,17 @@ namespace PepperDash.Essentials.Touchpanel
return base.CustomActivate();
}
+ ///
+ /// Stops the CommunicationMonitor on deactivation.
+ ///
+ /// True if deactivation was successful; otherwise, false.
+ public override bool Deactivate()
+ {
+ CommunicationMonitor?.Stop();
+
+ return base.Deactivate();
+ }
+
///
/// Handles device extender signal changes for system reserved signals.
///
@@ -519,15 +554,8 @@ namespace PepperDash.Essentials.Touchpanel
return false;
}) ? csIpAddress.ToString() : processorIp;
- var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app\?token=.+$");
- if (match.Success)
- {
- string ipa = match.Groups[1].Value;
- // ip will be "192.168.1.100"
- }
-
- // replace ipa with ip but leave the rest of the string intact
- var updatedUrl = Regex.Replace(url, @"^http://[^:/]+", $"http://{ip}");
+ // replace the host but preserve whatever scheme (http/https) is already present in the URL
+ var updatedUrl = Regex.Replace(url, @"^(https?)://[^:/]+", $"$1://{ip}");
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
@@ -740,7 +768,7 @@ namespace PepperDash.Essentials.Touchpanel
///
public MobileControlTouchpanelControllerFactory()
{
- TypeNames = new List() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" };
+ TypeNames = new List() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mctsw1080", "mcts1080", "mcxpanel", "mcdge1000" };
MinimumEssentialsFrameworkVersion = "2.0.0";
factories = new Dictionary>
@@ -765,6 +793,8 @@ namespace PepperDash.Essentials.Touchpanel
{"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)},
{"tsw1070", (id, controlSystem, projectName) => new Tsw1070(id, controlSystem)},
{"ts1070", (id, controlSystem, projectName) => new Ts1070(id, controlSystem)},
+ {"tsw1080", (id, controlSystem, projectName) => new Tsw1080(id, controlSystem)},
+ {"ts1080", (id, controlSystem, projectName) => new Ts1080(id, controlSystem)},
{"dge1000", (id, controlSystem, projectName) => new Dge1000(id, controlSystem)}
};
}
diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs
index 1c9ed37a..c96dc116 100644
--- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs
+++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs
@@ -127,6 +127,16 @@ namespace PepperDash.Essentials.WebSocketServer
///
public int Port { get; private set; }
+ ///
+ /// Gets the HTTP scheme to use for generated URLs, based on whether the direct server is configured as secure
+ ///
+ private string HttpScheme => _parent.Config.DirectServer.Secure ? "https" : "http";
+
+ ///
+ /// Gets the WebSocket scheme to use for generated URLs, based on whether the direct server is configured as secure
+ ///
+ private string WsScheme => _parent.Config.DirectServer.Secure ? "wss" : "ws";
+
///
/// Gets the user app URL prefix
///
@@ -134,7 +144,8 @@ namespace PepperDash.Essentials.WebSocketServer
{
get
{
- return string.Format("http://{0}:{1}{2}?token=",
+ return string.Format("{0}://{1}:{2}{3}?token=",
+ HttpScheme,
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
Port,
_userAppBaseHref);
@@ -273,7 +284,7 @@ namespace PepperDash.Essentials.WebSocketServer
{
base.Initialize();
- _server = new HttpServer(Port, false);
+ _server = new HttpServer(Port, _parent.Config.DirectServer.Secure);
_server.OnGet += Server_OnGet;
@@ -291,7 +302,7 @@ namespace PepperDash.Essentials.WebSocketServer
{
ClientCertificateRequired = false,
CheckCertificateRevocation = false,
- EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11
+ EnabledSslProtocols = SslProtocols.Tls12
};
}
@@ -403,11 +414,11 @@ namespace PepperDash.Essentials.WebSocketServer
ip = csIpAddress.ToString();
}
- var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
+ var appUrl = $"{HttpScheme}://{ip}:{Port}/mc/app?token={touchpanel.Key}";
this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key);
- touchpanel.Touchpanel.SetAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
+ touchpanel.Touchpanel.SetAppUrl(appUrl);
}
}
@@ -487,7 +498,7 @@ namespace PepperDash.Essentials.WebSocketServer
{
var config = new MobileControlApplicationConfig
{
- ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port),
+ ApiPath = string.Format("{0}://{1}:{2}/mc/api", HttpScheme, processorIp, Port),
GatewayAppPath = "",
LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
@@ -1098,6 +1109,7 @@ namespace PepperDash.Essentials.WebSocketServer
res.StatusCode = 200;
res.Close();
+ // remote log collector has no dedicated secure flag; keep it on http regardless of DirectServer.Secure
var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs")
{
Content = new StringContent(body, Encoding.UTF8, "application/json"),
@@ -1150,6 +1162,11 @@ namespace PepperDash.Essentials.WebSocketServer
var qp = req.QueryString;
var token = qp["token"];
+ // Each join mints a single-use clientId; the panel webview must never replay a cached
+ // response, or it reconnects forever with an already-consumed id (1008 loop).
+ res.Headers.Add("Cache-Control", "no-store");
+ res.Headers.Add("Pragma", "no-cache");
+
this.LogVerbose("Join Room Request with token: {token}", token);
byte[] body;
@@ -1213,8 +1230,7 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
// Construct WebSocket URL with clientId query parameter
- var wsProtocol = "ws";
- var wsUrl = $"{wsProtocol}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
+ var wsUrl = $"{WsScheme}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
// Construct the response object
JoinResponse jRes = new JoinResponse
@@ -1226,7 +1242,8 @@ namespace PepperDash.Essentials.WebSocketServer
Config = _parent.GetConfigWithPluginVersion(),
CodeExpires = new DateTime().AddYears(1),
UserCode = bridge.UserCode,
- UserAppUrl = string.Format("http://{0}:{1}/mc/app",
+ UserAppUrl = string.Format("{0}://{1}:{2}/mc/app",
+ HttpScheme,
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
Port),
WebSocketUrl = wsUrl,
@@ -1251,6 +1268,8 @@ namespace PepperDash.Essentials.WebSocketServer
{
res.StatusCode = 200;
res.ContentType = "application/json";
+ res.Headers.Add("Cache-Control", "no-store");
+ res.Headers.Add("Pragma", "no-cache");
var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
var message = JsonConvert.SerializeObject(version);
this.LogVerbose("{message}", message);
diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj
index 20a42ffd..cb83bf84 100644
--- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj
+++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj
@@ -48,7 +48,7 @@
-
+