Compare commits

..

46 commits

Author SHA1 Message Date
Neil Dorin
7f05654638
Merge pull request #1469 from PepperDash/hotfix/ssh-stopprog-timing
fix(core-ssh): stop reconnect loop during stopprog
2026-08-20 14:47:33 -06:00
Robert Sanders
4ca59e3186 fix(core-ssh): harden GenericSshClient shutdown reconnect guards 2026-08-20 16:39:20 -04:00
Robert Sanders
a55e4811b3 fix(core-ssh): stop reconnect loop during stopprog 2026-08-20 16:18:04 -04:00
Andrew Welker
5cac0bb6c8
Merge pull request #1468 from PepperDash/fix/ssh-connection-exception 2026-08-20 11:52:03 -05:00
Neil Dorin
44e5ff4c3e
fix: Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-20 10:47:30 -06:00
Neil Dorin
ec9753b19e fix(ssh-client): improve error handling for SSH connection exceptions 2026-08-20 10:26:09 -06:00
Jonathan Arndt
12e5ff752d
Merge pull request #1464 from PepperDash/fix-mobile-control-join-response-caching
fix(mobile-control): update version and improve response caching for …
2026-08-19 10:37:32 -07:00
Jonathan Arndt
85a4b635f6 fix(mobile-control): update version and improve response caching for join requests 2026-08-19 10:24:23 -07:00
Jonathan Arndt
aff4c1e897
Merge pull request #1450 from PepperDash/feature/mobile-control-https-wss-support
feat(mobile-control): support https/wss for direct server URLs when Secure is true
2026-08-13 16:27:47 -07:00
jkdevito
3afc73a446 fix(mobile-control): address PR review feedback
- Use the actual listening Port instead of raw DirectServer.Port config
  when building the app URL and API path, so advertised URLs match the
  listener even when the configured port is 0.
- Keep the remote logging endpoint on http:// since there is no
  dedicated secure flag for the log collector.
- Remove dead Regex.Match/ipa variable in GetUrlWithCorrectIp; the
  scheme-preserving Regex.Replace below it already handles this.
2026-08-13 17:50:26 -05:00
jkdevito
3c9f5e95e0 feat(mobile-control): support https/wss for direct server URLs when Secure is true
- Add HttpScheme/WsScheme helpers driven by directServer.Secure
- Replace hardcoded http/ws literals in UserAppUrlPrefix, touchpanel app URL,
  _config.local.json ApiPath, remote logging POST, join-response WebSocketUrl
  and UserAppUrl
- Pass Secure flag into the HttpServer constructor so the listener actually
  negotiates TLS when a cert is configured
- Drop TLS 1.1 from EnabledSslProtocols, keep TLS 1.2 (TLS 1.3 is not defined
  in the net472 SslProtocols enum used by this project)
- MobileControlTouchpanelController: rewrite the app-URL IP regex to match
  and preserve either http or https instead of assuming http
2026-08-13 17:46:17 -05:00
Neil Dorin
f70c78bcc5
Merge pull request #1456 from PepperDash/feat/touchpanel-communication-monitor 2026-08-12 21:14:52 -06:00
Neil Dorin
5b5619764f
Merge pull request #1455 from PepperDash/feature/touchpanel-1080-types 2026-08-12 21:13:29 -06:00
Jonathan Arndt
d21cef0e60
fix: Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-30 16:22:30 -07:00
Jonathan Arndt
1719de195f feat: implement ICommunicationMonitor on MobileControlTouchpanelController for Health page visibility 2026-07-30 14:10:15 -07:00
Jonathan Arndt
6949253b1a feat: add TS-1080 and TSW-1080 touchpanel support
Add mctsw1080 and mcts1080 Mobile Control touchpanel types and map them to
the Tsw1080 and Ts1080 hardware classes in the touchpanel controller factory.

Bump the Crestron SimplSharp SDK packages (Library, ProgramLibrary, Program)
from 2.21.90 to 2.21.274 across all projects. The Tsw1080 and Ts1080 device
classes do not exist in the SDK prior to 2.21.274, so the SDK bump is required
for the new panel types to compile.
2026-07-29 14:39:36 -07:00
Andrew Knous
e23c987516
Merge pull request #1448 from PepperDash/fix-cecportcontroller-null-streamcec-guard
Fix cecportcontroller null streamcec guard
2026-07-16 09:40:19 -04:00
copilot-swe-agent[bot]
14cd082076
Remove redundant IRoutingInputsOutputs filtering in route mapping loops 2026-07-16 13:26:25 +00:00
copilot-swe-agent[bot]
36c15b8d95
Clarify CecPortController subscription XML comment 2026-07-16 13:25:25 +00:00
erikdred
fbf4c56403
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-16 09:23:18 -04:00
erikdred
f4cb17314d
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-16 09:21:45 -04:00
Erik Meyer
aa76551500 fix: guard CecPortController against null StreamCec and lazily subscribe 2026-07-16 08:40:33 -04:00
Neil Dorin
12828e8dd4
Merge pull request #1446 from PepperDash/feature-add-iHasDspPresetSave
feat: add IHasDspPresetSave interface for DSP preset management
2026-07-14 11:29:44 -06:00
Jonathan Arndt
4dca16e9bd
fix: Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-13 16:09:45 -07:00
Jonathan Arndt
d4284bd59f
fix: Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-13 16:08:10 -07:00
Jonathan Arndt
a8ec831064 feat: add IHasDspPresetSave interface for DSP preset management 2026-07-13 12:58:52 -07:00
aknous
8d3edde28c feat: adds config props to disable power on/off automation 2026-06-17 10:06:00 -04:00
equinoy
a220474101
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-15 10:05:50 -05:00
equinoy
b982219e2f
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-15 10:05:36 -05:00
equinoy
e03e45ccf5
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-15 10:05:25 -05:00
equinoy
a7b839296e
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-15 10:05:14 -05:00
jkdevito
6a4e3d5be2 feat: add port forwarding functionality for debug websocket in DebugSessionRequestHandler 2026-06-12 11:26:56 -05:00
Neil Dorin
d18fca8a98 fix: handle null properties in DeviceMessageBase and DeviceStateMessageBase 2026-06-11 17:36:30 -04:00
aknous
162a06f9e9 feat: adds interface for wireless sharing 2026-06-11 09:41:39 -04:00
Andrew Welker
b32bab0d33 fix: remove impossibleRoutes cache
The cache wasn't being cleared correctly, and was an unnecessary
add.
2026-06-09 09:32:59 -05:00
Neil Dorin
74a12c9671 fix: add null or empty checks for midpoint keys in RoutingFeedbackManager 2026-05-21 18:47:25 -06:00
Neil Dorin
92d13f29a9 fix: refine routing logic by filtering IRoutingInputs and IRoutingOutputs, enhance logging for route mapping 2026-05-21 16:43:00 -06:00
Neil Dorin
6587444970 fix: update routing logic and enhance logging in Extensions and MockVC classes 2026-05-21 13:11:39 -06:00
Nick Genovese
94c9e75d8f Merge branch 'main' into feature/web-api-updates 2026-05-21 10:27:02 -04:00
Erik Meyer
21217341b5 Merge branch 'main' into feature/web-api-updates 2026-05-19 12:05:48 -04:00
Neil Dorin
d0ab42ccb7 Merge branch 'feature/add-network-switch-poe-interfaces' into feature/web-api-updates 2026-04-01 08:56:21 -06:00
Neil Dorin
1b10910cc2 fix: update GetFeedbacksForDeviceRequestHandler to return JSON response for missing device 2026-04-01 08:56:10 -06:00
Neil Dorin
84c730b7a1 feat: enhance NetworkSwitchPortEventType enum with additional states for VLAN and PoE changes 2026-03-20 14:53:21 -06:00
Neil Dorin
6db7581295 feat: add Unknown event type to NetworkSwitchPortEventType enum for improved event handling 2026-03-20 14:48:58 -06:00
Neil Dorin
76311b83ff feat: add event and arguments for port state changes in network switch management 2026-03-20 13:25:58 -06:00
Neil Dorin
8817d70f07 feat: add interfaces for VLAN and PoE management in network switches 2026-03-20 12:03:00 -06:00
20 changed files with 383 additions and 68 deletions

View file

@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>2.36.6-local</Version> <Version>2.42.1-local</Version>
<InformationalVersion>$(Version)</InformationalVersion> <InformationalVersion>$(Version)</InformationalVersion>
<Authors>PepperDash Technology</Authors> <Authors>PepperDash Technology</Authors>
<Company>PepperDash Technology</Company> <Company>PepperDash Technology</Company>

View file

@ -151,6 +151,8 @@ namespace PepperDash.Core
// Thread-safety lock for state changes // Thread-safety lock for state changes
private readonly object _stateLock = new object(); private readonly object _stateLock = new object();
private volatile bool _isProgramStopping;
private bool disconnectLogged = false; private bool disconnectLogged = false;
/// <summary> /// <summary>
@ -207,11 +209,9 @@ namespace PepperDash.Core
{ {
if (programEventType == eProgramStatusEventType.Stopping) if (programEventType == eProgramStatusEventType.Stopping)
{ {
if (client != null) _isProgramStopping = true;
{ this.LogDebug("Program stopping. Closing connection");
this.LogDebug("Program stopping. Closing connection"); Disconnect();
Disconnect();
}
} }
} }
@ -228,6 +228,12 @@ namespace PepperDash.Core
return; return;
} }
if (_isProgramStopping)
{
this.LogDebug("Skipping connect because program is stopping");
return;
}
ConnectEnabled = true; ConnectEnabled = true;
try try
@ -287,13 +293,7 @@ namespace PepperDash.Core
} }
catch (SshConnectionException e) catch (SshConnectionException e)
{ {
var ie = e.InnerException; // The details are inside!! var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all
if (ie is SocketException)
{
this.LogError("CONNECTION failure: Cannot reach host");
this.LogVerbose(ie, "Exception details: ");
}
if (ie is System.Net.Sockets.SocketException socketException) if (ie is System.Net.Sockets.SocketException socketException)
{ {
@ -301,20 +301,20 @@ namespace PepperDash.Core
Hostname, Port); Hostname, Port);
this.LogVerbose(socketException, "SocketException details: "); this.LogVerbose(socketException, "SocketException details: ");
} }
if (ie is SshAuthenticationException) else if (ie is SshAuthenticationException)
{ {
this.LogError("Authentication failure for username {userName}", Username); this.LogError("Authentication failure for username {userName}", Username);
this.LogVerbose(ie, "AuthenticationException details: "); this.LogVerbose(ie, "AuthenticationException details: ");
} }
else else
{ {
this.LogError("Error on connect: {error}", ie.Message); this.LogError("Error on connect: {error}", ie?.Message ?? e.Message);
this.LogVerbose(ie, "Exception details: "); this.LogVerbose(ie ?? e, "Exception details: ");
} }
disconnectLogged = true; disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect) if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{ {
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); StartReconnectTimer();
@ -326,7 +326,7 @@ namespace PepperDash.Core
disconnectLogged = true; disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect) if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); StartReconnectTimer();
@ -338,7 +338,7 @@ namespace PepperDash.Core
this.LogVerbose(e, "Exception details: "); this.LogVerbose(e, "Exception details: ");
disconnectLogged = true; disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect) if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); StartReconnectTimer();
@ -473,7 +473,7 @@ namespace PepperDash.Core
{ {
connectLock.Release(); connectLock.Release();
} }
if (AutoReconnect && ConnectEnabled) if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); StartReconnectTimer();
@ -516,7 +516,10 @@ namespace PepperDash.Core
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim()); this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
StartReconnectTimer(); if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{
StartReconnectTimer();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -549,7 +552,10 @@ namespace PepperDash.Core
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes)); this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
StartReconnectTimer(); if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
{
StartReconnectTimer();
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -43,7 +43,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" /> <PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" /> <PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.274" />
<PackageReference Include="Serilog" Version="3.1.1" /> <PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Expressions" Version="4.0.0" /> <PackageReference Include="Serilog.Expressions" Version="4.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" /> <PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />

View file

@ -39,6 +39,8 @@ namespace PepperDash.Essentials.Core
ICec Port; ICec Port;
bool _cecSubscribed;
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>
@ -54,7 +56,7 @@ namespace PepperDash.Essentials.Core
{ {
Port = postActivationFunc(config); Port = postActivationFunc(config);
Port.StreamCec.CecChange += StreamCec_CecChange; TryEnsureCecSubscription();
}); });
} }
@ -68,7 +70,29 @@ namespace PepperDash.Essentials.Core
{ {
Port = port; Port = port;
TryEnsureCecSubscription();
}
/// <summary>
/// Subscribes to the CEC change event once <see cref="ICec.StreamCec"/> 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.
/// </summary>
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); 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) void StreamCec_CecChange(Cec cecDevice, CecEventArgs args)
@ -104,8 +128,9 @@ namespace PepperDash.Essentials.Core
/// </summary> /// </summary>
public void SendText(string text) public void SendText(string text)
{ {
if (Port == null) if (Port?.StreamCec == null)
return; return;
TryEnsureCecSubscription();
this.PrintSentText(text); this.PrintSentText(text);
Port.StreamCec.Send.StringValue = text; Port.StreamCec.Send.StringValue = text;
} }
@ -115,8 +140,9 @@ namespace PepperDash.Essentials.Core
/// </summary> /// </summary>
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
if (Port == null) if (Port?.StreamCec == null)
return; return;
TryEnsureCecSubscription();
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
this.PrintSentBytes(bytes); this.PrintSentBytes(bytes);
Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes)); Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));

View file

@ -0,0 +1,48 @@
using System;
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// This refers specifically to wireless screen/device mirroring, as distinct from in-call
/// content sharing on a video conference.
/// </remarks>
public interface IHasWirelessSharing
{
/// <summary>
/// Reports whether a wireless sharing session is currently active (content is being presented).
/// </summary>
BoolFeedback IsSharingFeedback { get; }
/// <summary>
/// Raised when wireless sharing starts or stops. The event args carry the new sharing state.
/// </summary>
event EventHandler<WirelessSharingEventArgs> SharingChanged;
}
/// <summary>
/// Event arguments describing a change in wireless sharing state.
/// </summary>
public class WirelessSharingEventArgs : EventArgs
{
/// <summary>
/// True if a wireless sharing session is active (content is being presented), false otherwise.
/// </summary>
public bool IsSharing { get; private set; }
/// <summary>
/// Creates a new <see cref="WirelessSharingEventArgs"/>.
/// </summary>
/// <param name="isSharing">True if a wireless sharing session is active, false otherwise.</param>
public WirelessSharingEventArgs(bool isSharing)
{
IsSharing = isSharing;
}
}
}

View file

@ -0,0 +1,122 @@
using System;
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
/// <summary>
/// Interface for network switches that support VLAN assignment on individual ports.
/// </summary>
public interface INetworkSwitchVlanManager
{
/// <summary>
/// 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).
/// </summary>
/// <param name="port">Switch port identifier</param>
/// <returns>VLAN ID or -1 when unavailable</returns>
int GetPortCurrentVlan(string port);
/// <summary>
/// Changes the access VLAN of a single switch port.
/// The implementation is responsible for entering/exiting privileged/config mode.
/// </summary>
/// <param name="port">Switch port identifier (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco)</param>
/// <param name="vlanId">Target VLAN ID (1-4093)</param>
void SetPortVlan(string port, uint vlanId);
}
/// <summary>
/// Interface for network switches that support Power over Ethernet (PoE) control on individual ports.
/// </summary>
public interface INetworkSwitchPoeManager
{
/// <summary>
/// Enables or disables PoE power delivery on a single switch port.
/// The implementation is responsible for entering/exiting privileged/config mode.
/// </summary>
/// <param name="port">Switch port identifier</param>
/// <param name="enabled">True to enable PoE; false to disable PoE</param>
void SetPortPoeState(string port, bool enabled);
}
/// <summary>
/// Standardized interface for network switch devices that support per-port PoE control
/// and VLAN assignment.
/// </summary>
public interface INetworkSwitchPoeVlanManager : INetworkSwitchVlanManager, INetworkSwitchPoeManager
{
/// <summary>
/// Event that is raised when the state of a switch port changes, such as a VLAN change or PoE state change.
/// </summary>
event EventHandler<NetworkSwitchPortEventArgs> PortStateChanged;
}
/// <summary>
/// Event arguments for port state changes on a network switch, such as VLAN changes or PoE state changes.
/// </summary>
public class NetworkSwitchPortEventArgs : EventArgs
{
/// <summary>
/// The identifier of the port that changed state (e.g. "1/0/3" for Netgear, "gi1/0/3" for Cisco).
/// </summary>
public string Port { get; private set; }
/// <summary>
/// The type of event that occurred on the port (e.g. VLAN change, PoE enabled/disabled).
/// </summary>
public NetworkSwitchPortEventType EventType { get; private set; }
/// <summary>
/// Constructor for NetworkSwitchPortEventArgs
/// </summary>
/// <param name="port">The identifier of the port that changed state</param>
/// <param name="eventType">The type of event that occurred on the port</param>
public NetworkSwitchPortEventArgs(string port, NetworkSwitchPortEventType eventType)
{
Port = port;
EventType = eventType;
}
}
/// <summary>
/// Enumeration of network switch port state change event types (e.g. VLAN changes or PoE state changes).
/// </summary>
public enum NetworkSwitchPortEventType
{
/// <summary>
/// Indicates that the type of event is unknown or cannot be determined.
/// </summary>
Unknown,
/// <summary>
/// Indicates that a VLAN change is in progress on the port, either through a call to SetPortVlan or an external change detected by polling.
/// </summary>
VlanChangeInProgress,
/// <summary>
/// Indicates that the access VLAN on a port has changed, either through a successful call to SetPortVlan or an external change detected by polling.
/// </summary>
VlanChanged,
/// <summary>
/// Indicates that PoE is being disabled on the port, either through a call to SetPortPoeState or an external change detected by polling.
/// </summary>
PoeDisableInProgress,
/// <summary>
/// Indicates that PoE has been disabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling.
/// </summary>
PoEDisabled,
/// <summary>
/// Indicates that PoE is being enabled on the port, either through a call to SetPortPoeState or an external change detected by polling.
/// </summary>
PoeEnableInProgress,
/// <summary>
/// Indicates that PoE has been enabled on the port, either through a successful call to SetPortPoeState or an external change detected by polling.
/// </summary>
PoEEnabled
}
}

View file

@ -0,0 +1,14 @@
namespace PepperDash.Essentials.Core
{
/// <summary>
/// Defines the contract for IHasDspPresetSave
/// </summary>
public interface IHasDspPresetSave : IDspPresets
{
/// <summary>
/// Saves the preset by key
/// </summary>
/// <param name="key">key of preset to save</param>
void SavePreset(string key);
}
}

View file

@ -25,7 +25,7 @@
<DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile> <DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" /> <PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" /> <None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />

View file

@ -343,12 +343,14 @@ namespace PepperDash.Essentials.Core
IndexTieLines(); IndexTieLines();
} }
var sinks = DeviceManager.AllDevices.OfType<IRoutingInputs>(); var sinks = DeviceManager.AllDevices.OfType<IRoutingInputs>()
var sources = DeviceManager.AllDevices.OfType<IRoutingOutputs>(); .Where(d => !(d is IRoutingInputsOutputs)).ToList();
var sources = DeviceManager.AllDevices.OfType<IRoutingOutputs>()
.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) foreach (var inputPort in sink.InputPorts)
{ {
@ -373,6 +375,10 @@ namespace PepperDash.Essentials.Core
continue; 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 // 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 // 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)) if (audioOrSingleRoute.SignalType.HasFlag(eRoutingSignalType.Audio))
@ -404,6 +410,10 @@ namespace PepperDash.Essentials.Core
continue; continue;
} }
Debug.LogVerbose("Video route mapped: {source} -> {sink} via {input}/{output}",
source.Key, sink.Key,
inputPort.Key, outputPort.Key);
RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute); RouteDescriptors[eRoutingSignalType.Video].AddRouteDescriptor(videoRoute);
} }
} }
@ -609,10 +619,12 @@ namespace PepperDash.Essentials.Core
// No direct tie? Run back out on the inputs' attached devices... // No direct tie? Run back out on the inputs' attached devices...
// Only the ones that are routing devices // Only the ones that are routing devices
var midpointTieLines = destinationTieLines.Where(t => t.SourcePort.ParentDevice is IRoutingInputsOutputs); 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); .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 //Create a list for tracking already checked devices to avoid loops, if it doesn't already exist from previous iteration
if (alreadyCheckedDevices == null) if (alreadyCheckedDevices == null)
alreadyCheckedDevices = new List<IRoutingInputsOutputs>(); alreadyCheckedDevices = new List<IRoutingInputsOutputs>();

View file

@ -1,13 +1,14 @@
using System.Linq; using System;
using System.Linq;
using Crestron.SimplSharp.WebScripting; using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json; using Newtonsoft.Json;
using PepperDash.Core.Web.RequestHandlers; using PepperDash.Core.Web.RequestHandlers;
namespace PepperDash.Essentials.Core.Web.RequestHandlers namespace PepperDash.Essentials.Core.Web.RequestHandlers
{ {
/// <summary> /// <summary>
/// Represents a GetFeedbacksForDeviceRequestHandler /// Represents a GetFeedbacksForDeviceRequestHandler
/// </summary> /// </summary>
public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler public class GetFeedbacksForDeviceRequestHandler : WebApiBaseRequestHandler
{ {
/// <summary> /// <summary>
@ -76,7 +77,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
Value = feedback.IntValue Value = feedback.IntValue
}; };
var stringFeedback = var stringFeedback =
from feedback in device.Feedbacks.OfType<StringFeedback>() from feedback in device.Feedbacks.OfType<StringFeedback>()
where !string.IsNullOrEmpty(feedback.Key) where !string.IsNullOrEmpty(feedback.Key)
select new select new

View file

@ -152,6 +152,15 @@ namespace PepperDash.Essentials.Devices.Common.Shades
private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e) 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 ( if (
!DisplayDevice.IsCoolingDownFeedback.BoolValue !DisplayDevice.IsCoolingDownFeedback.BoolValue
&& Type == eScreenLiftControlType.lift && Type == eScreenLiftControlType.lift
@ -174,6 +183,15 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{ {
if (DisplayDevice.IsWarmingUpFeedback.BoolValue) 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(); Lower();
} }
} }

View file

@ -41,5 +41,23 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary> /// </summary>
[JsonProperty("muteOnScreenUp")] [JsonProperty("muteOnScreenUp")]
public bool MuteOnScreenUp { get; set; } public bool MuteOnScreenUp { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonProperty("disableAutoLowerOnPowerOn")]
public bool DisableAutoLowerOnPowerOn { get; set; }
/// <summary>
/// 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 <see cref="DisableAutoLowerOnPowerOn"/>; together they make a controller fully
/// manual while leaving other controllers (e.g. the lift) on their default automatic behavior.
/// </summary>
[JsonProperty("disableAutoRaiseOnPowerOff")]
public bool DisableAutoRaiseOnPowerOff { get; set; }
} }
} }

View file

@ -29,6 +29,6 @@
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" /> <ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" /> <PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <summary> /// <summary>
/// The device key /// The device key
/// </summary> /// </summary>
[JsonProperty("key")] [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
/// <summary> /// <summary>
/// Gets or sets the Key /// Gets or sets the Key
/// </summary> /// </summary>
@ -19,19 +19,19 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <summary> /// <summary>
/// The device name /// The device name
/// </summary> /// </summary>
[JsonProperty("name")] [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// The type of the message class /// The type of the message class
/// </summary> /// </summary>
[JsonProperty("messageType")] [JsonProperty("messageType", NullValueHandling = NullValueHandling.Ignore)]
public string MessageType => GetType().Name; public string MessageType => GetType().Name;
/// <summary> /// <summary>
/// Gets or sets the MessageBasePath /// Gets or sets the MessageBasePath
/// </summary> /// </summary>
[JsonProperty("messageBasePath")] [JsonProperty("messageBasePath", NullValueHandling = NullValueHandling.Ignore)]
public string MessageBasePath { get; set; } public string MessageBasePath { get; set; }
} }

View file

@ -12,7 +12,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <summary> /// <summary>
/// The interfaces implmented by the device sending the messsage /// The interfaces implmented by the device sending the messsage
/// </summary> /// </summary>
[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<string> Interfaces { get; private set; } public List<string> Interfaces { get; private set; }
/// <summary> /// <summary>

View file

@ -33,7 +33,7 @@
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" /> <Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" /> <PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" /> <ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />

View file

@ -38,7 +38,7 @@
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" /> <Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" /> <PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" /> <PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View file

@ -25,11 +25,28 @@ namespace PepperDash.Essentials.Touchpanel
/// Mobile Control touchpanel controller that provides app control, Zoom integration, /// Mobile Control touchpanel controller that provides app control, Zoom integration,
/// and mobile control functionality for Crestron touchpanels. /// and mobile control functionality for Crestron touchpanels.
/// </summary> /// </summary>
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 readonly MobileControlTouchpanelProperties localConfig;
private IMobileControlRoomMessenger _bridge; private IMobileControlRoomMessenger _bridge;
/// <summary>
/// Gets the CommunicationMonitor tracking the panel's online/offline state
/// </summary>
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; private string _appUrl;
/// <summary> /// <summary>
@ -128,6 +145,11 @@ namespace PepperDash.Essentials.Touchpanel
{ {
localConfig = config; localConfig = config;
if (panel != null)
{
CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, panel, 120000, 300000);
}
AddPostActivationAction(SubscribeForMobileControlUpdates); AddPostActivationAction(SubscribeForMobileControlUpdates);
ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme); ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme);
@ -366,6 +388,8 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary> /// </summary>
public override bool CustomActivate() public override bool CustomActivate()
{ {
CommunicationMonitor?.Start();
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this); var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this); var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this);
@ -393,6 +417,17 @@ namespace PepperDash.Essentials.Touchpanel
return base.CustomActivate(); return base.CustomActivate();
} }
/// <summary>
/// Stops the CommunicationMonitor on deactivation.
/// </summary>
/// <returns>True if deactivation was successful; otherwise, false.</returns>
public override bool Deactivate()
{
CommunicationMonitor?.Stop();
return base.Deactivate();
}
/// <summary> /// <summary>
/// Handles device extender signal changes for system reserved signals. /// Handles device extender signal changes for system reserved signals.
/// </summary> /// </summary>
@ -519,15 +554,8 @@ namespace PepperDash.Essentials.Touchpanel
return false; return false;
}) ? csIpAddress.ToString() : processorIp; }) ? csIpAddress.ToString() : processorIp;
var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app\?token=.+$"); // replace the host but preserve whatever scheme (http/https) is already present in the URL
if (match.Success) var updatedUrl = Regex.Replace(url, @"^(https?)://[^:/]+", $"$1://{ip}");
{
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}");
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl); this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
@ -740,7 +768,7 @@ namespace PepperDash.Essentials.Touchpanel
/// </summary> /// </summary>
public MobileControlTouchpanelControllerFactory() public MobileControlTouchpanelControllerFactory()
{ {
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" }; TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mctsw1080", "mcts1080", "mcxpanel", "mcdge1000" };
MinimumEssentialsFrameworkVersion = "2.0.0"; MinimumEssentialsFrameworkVersion = "2.0.0";
factories = new Dictionary<string, Func<uint, CrestronControlSystem, string, BasicTriListWithSmartObject>> factories = new Dictionary<string, Func<uint, CrestronControlSystem, string, BasicTriListWithSmartObject>>
@ -765,6 +793,8 @@ namespace PepperDash.Essentials.Touchpanel
{"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)}, {"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)},
{"tsw1070", (id, controlSystem, projectName) => new Tsw1070(id, controlSystem)}, {"tsw1070", (id, controlSystem, projectName) => new Tsw1070(id, controlSystem)},
{"ts1070", (id, controlSystem, projectName) => new Ts1070(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)} {"dge1000", (id, controlSystem, projectName) => new Dge1000(id, controlSystem)}
}; };
} }

View file

@ -127,6 +127,16 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary> /// </summary>
public int Port { get; private set; } public int Port { get; private set; }
/// <summary>
/// Gets the HTTP scheme to use for generated URLs, based on whether the direct server is configured as secure
/// </summary>
private string HttpScheme => _parent.Config.DirectServer.Secure ? "https" : "http";
/// <summary>
/// Gets the WebSocket scheme to use for generated URLs, based on whether the direct server is configured as secure
/// </summary>
private string WsScheme => _parent.Config.DirectServer.Secure ? "wss" : "ws";
/// <summary> /// <summary>
/// Gets the user app URL prefix /// Gets the user app URL prefix
/// </summary> /// </summary>
@ -134,7 +144,8 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
get 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), CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
Port, Port,
_userAppBaseHref); _userAppBaseHref);
@ -273,7 +284,7 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
base.Initialize(); base.Initialize();
_server = new HttpServer(Port, false); _server = new HttpServer(Port, _parent.Config.DirectServer.Secure);
_server.OnGet += Server_OnGet; _server.OnGet += Server_OnGet;
@ -291,7 +302,7 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
ClientCertificateRequired = false, ClientCertificateRequired = false,
CheckCertificateRevocation = false, CheckCertificateRevocation = false,
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 EnabledSslProtocols = SslProtocols.Tls12
}; };
} }
@ -403,11 +414,11 @@ namespace PepperDash.Essentials.WebSocketServer
ip = csIpAddress.ToString(); 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); 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 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 = "", GatewayAppPath = "",
LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png", LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false, EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
@ -1098,6 +1109,7 @@ namespace PepperDash.Essentials.WebSocketServer
res.StatusCode = 200; res.StatusCode = 200;
res.Close(); 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") 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"), Content = new StringContent(body, Encoding.UTF8, "application/json"),
@ -1150,6 +1162,11 @@ namespace PepperDash.Essentials.WebSocketServer
var qp = req.QueryString; var qp = req.QueryString;
var token = qp["token"]; 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); this.LogVerbose("Join Room Request with token: {token}", token);
byte[] body; byte[] body;
@ -1213,8 +1230,7 @@ namespace PepperDash.Essentials.WebSocketServer
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now); this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
// Construct WebSocket URL with clientId query parameter // Construct WebSocket URL with clientId query parameter
var wsProtocol = "ws"; var wsUrl = $"{WsScheme}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
var wsUrl = $"{wsProtocol}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
// Construct the response object // Construct the response object
JoinResponse jRes = new JoinResponse JoinResponse jRes = new JoinResponse
@ -1226,7 +1242,8 @@ namespace PepperDash.Essentials.WebSocketServer
Config = _parent.GetConfigWithPluginVersion(), Config = _parent.GetConfigWithPluginVersion(),
CodeExpires = new DateTime().AddYears(1), CodeExpires = new DateTime().AddYears(1),
UserCode = bridge.UserCode, 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), CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
Port), Port),
WebSocketUrl = wsUrl, WebSocketUrl = wsUrl,
@ -1251,6 +1268,8 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
res.StatusCode = 200; res.StatusCode = 200;
res.ContentType = "application/json"; 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 version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
var message = JsonConvert.SerializeObject(version); var message = JsonConvert.SerializeObject(version);
this.LogVerbose("{message}", message); this.LogVerbose("{message}", message);

View file

@ -48,7 +48,7 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" /> <PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.274" />
<PackageReference Include="System.IO.Compression" Version="4.0.0" /> <PackageReference Include="System.IO.Compression" Version="4.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>