Compare commits

...

16 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
10 changed files with 103 additions and 48 deletions

View file

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

View file

@ -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;
/// <summary>
@ -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)
{

View file

@ -43,7 +43,7 @@
</ItemGroup>
<ItemGroup>
<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.Expressions" Version="4.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />

View file

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

View file

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

View file

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

View file

@ -38,7 +38,7 @@
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
</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" />
</ItemGroup>
<ItemGroup>

View file

@ -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.
/// </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 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;
/// <summary>
@ -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
/// </summary>
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();
}
/// <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>
/// Handles device extender signal changes for system reserved signals.
/// </summary>
@ -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
/// </summary>
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";
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)},
{"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)}
};
}

View file

@ -127,6 +127,16 @@ namespace PepperDash.Essentials.WebSocketServer
/// </summary>
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>
/// Gets the user app URL prefix
/// </summary>
@ -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);

View file

@ -48,7 +48,7 @@
</None>
</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" />
</ItemGroup>
<ItemGroup>