Compare commits

..

10 commits

Author SHA1 Message Date
Nick Genovese
f64d8a3f11 fix: Use RegisterWithLogging for touchpanel registration
Replaced _touchpanel.Register() with _touchpanel.RegisterWithLogging() to add logging during touchpanel registration, improving debugging and monitoring capabilities.
2026-08-14 10:36:08 -04:00
Nick Genovese
21faed417b fix: Refactor activation logic and improve logging
Replaced Initialize() with CustomActivate() returning a bool for activation success. Added logging for activation and slot usage. Now returns false if slot not found and returns registration status at the end.
2026-08-14 10:35:07 -04:00
Nick Genovese
1eed8a5c34 feat: Update Crestron.SimplSharp.SDK NuGet packages
Upgraded Crestron.SimplSharp.SDK.Library, ProgramLibrary, and Program package versions from 2.21.90 to 2.21.274 across multiple project files. No other code or configuration changes were made.
2026-08-14 10:17:02 -04:00
Nick Genovese
cdabbdb164 fix: Refactor Mpc4TouchpanelController initialization logic
Refactored Mpc4TouchpanelController to store the processor as a private readonly field and moved button dictionary initialization to the constructor. Moved touchpanel slot selection logic from the constructor to an overridden Initialize() method, now using the _processor field and removing fallback to ControllerTouchScreenSlotDevice. Improved error logging by removing references to processor type and key variables. Changed button initialization to occur in Initialize() with a foreach loop, eliminating AddPostActivationAction and null checks for the button dictionary.
2026-08-14 09:17:47 -04:00
Nick Genovese
83bf40491b fix: Refine touchpanel device selection and logging
Improve logic to check for MPC3Basic type using pattern matching. Update debug and error messages to report actual device type. Clarify conditional structure for better readability.
2026-08-12 14:55:04 -04:00
Nick Genovese
25693c7071 fix: Add debug logs for touchscreen slot selection in controller
Improved logging to indicate which touchscreen slot is used during Mpc4TouchpanelController initialization. Updated error log to report processor.TouchscreenType for clearer diagnostics.
2026-08-12 14:40:54 -04:00
Nick Genovese
8c56623641 fix: Support multiple MPC4 touchscreen slot types in ctor
Updated Mpc4TouchpanelController constructor to assign _touchpanel from the first available slot among MPC4x102, MPC4x201, MPC4x301, or MPC4x302 touchscreen slots, improving compatibility with various MPC4 touchscreen models.
2026-08-12 14:17:50 -04:00
Nick Genovese
31fd665594 fix: Simplify touchscreen slot selection logic
Replaced switch statement for touchscreen type selection with a single cast assignment to MPC3Basic. This streamlines initialization and removes explicit handling for each touchscreen type. Error logging is retained for failed casts.
2026-08-12 12:44:43 -04:00
Nick Genovese
d713d65c7a fix: Refactor touchpanel init and error logging
Change _touchpanel type to MPC3Basic. Update constructor to select touchscreen slot by TouchscreenType using switch. Improve error logging with error level and detailed type info.
2026-08-12 12:35:21 -04:00
Nick Genovese
f5d6a076ad feat: Add Mpc4TouchpanelController for MPC4 processors
Introduced Mpc4TouchpanelController to manage touchpanel behavior for MPC4 class processors, including button initialization, feedback, and event handling. Updated ControlSystem.cs to detect MPC4 models, deserialize button configs, and register the controller with DeviceManager.
2026-08-12 11:22:03 -04:00
6 changed files with 387 additions and 97 deletions

View file

@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<Version>2.42.1-local</Version> <Version>2.36.6-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,8 +151,6 @@ 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>
@ -209,9 +207,11 @@ namespace PepperDash.Core
{ {
if (programEventType == eProgramStatusEventType.Stopping) if (programEventType == eProgramStatusEventType.Stopping)
{ {
_isProgramStopping = true; if (client != null)
this.LogDebug("Program stopping. Closing connection"); {
Disconnect(); this.LogDebug("Program stopping. Closing connection");
Disconnect();
}
} }
} }
@ -228,12 +228,6 @@ namespace PepperDash.Core
return; return;
} }
if (_isProgramStopping)
{
this.LogDebug("Skipping connect because program is stopping");
return;
}
ConnectEnabled = true; ConnectEnabled = true;
try try
@ -293,7 +287,13 @@ namespace PepperDash.Core
} }
catch (SshConnectionException e) catch (SshConnectionException e)
{ {
var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all var ie = e.InnerException; // The details are inside!!
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: ");
} }
else if (ie is SshAuthenticationException) 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 ?? e.Message); this.LogError("Error on connect: {error}", ie.Message);
this.LogVerbose(ie ?? e, "Exception details: "); this.LogVerbose(ie, "Exception details: ");
} }
disconnectLogged = true; disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect && ConnectEnabled && !_isProgramStopping) if (AutoReconnect)
{ {
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 && ConnectEnabled && !_isProgramStopping) if (AutoReconnect)
{ {
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 && ConnectEnabled && !_isProgramStopping) if (AutoReconnect)
{ {
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 && !_isProgramStopping) if (AutoReconnect && ConnectEnabled)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
StartReconnectTimer(); StartReconnectTimer();
@ -516,10 +516,7 @@ 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);
if (AutoReconnect && ConnectEnabled && !_isProgramStopping) StartReconnectTimer();
{
StartReconnectTimer();
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -552,10 +549,7 @@ 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);
if (AutoReconnect && ConnectEnabled && !_isProgramStopping) StartReconnectTimer();
{
StartReconnectTimer();
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View file

@ -0,0 +1,326 @@
using Crestron.SimplSharpPro;
using PepperDash.Core;
using PepperDash.Core.Logging;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Globalization;
namespace PepperDash.Essentials.Core.Touchpanels
{
/// <summary>
/// A wrapper class for the touchpanel portion of an MPC4 class process to allow for configurable
/// behavior of the keypad buttons
/// </summary>
public class Mpc4TouchpanelController : Device
{
private readonly CrestronControlSystem _processor;
private MPC3Basic _touchpanel;
readonly Dictionary<string, KeypadButton> _buttons;
/// <summary>
/// Constructor
/// </summary>
/// <param name="key">device key</param>
/// <param name="name">device name</param>
/// <param name="processor">control system processor</param>
/// <param name="buttons">dictionary of keypad buttons</param>
public Mpc4TouchpanelController(string key, string name, CrestronControlSystem processor, Dictionary<string, KeypadButton> buttons)
: base(key, name)
{
_processor = processor;
_buttons = buttons ?? new Dictionary<string, KeypadButton>();
}
public override bool CustomActivate()
{
Debug.LogInformation(this, "Activating MPC4 Touchpanel Controller with key {0}", Key);
if (_processor.MPC4x102TouchscreenSlot != null)
{
Debug.LogMessage(LogEventLevel.Information, this, "Using MPC4x102TouchscreenSlot");
_touchpanel = _processor.MPC4x102TouchscreenSlot;
}
else if (_processor.MPC4x201TouchscreenSlot != null)
{
Debug.LogMessage(LogEventLevel.Information, this, "Using MPC4x201TouchscreenSlot");
_touchpanel = _processor.MPC4x201TouchscreenSlot;
}
else if (_processor.MPC4x301TouchscreenSlot != null)
{
Debug.LogMessage(LogEventLevel.Information, this, "Using MPC4x301TouchscreenSlot");
_touchpanel = _processor.MPC4x301TouchscreenSlot;
}
else if (_processor.MPC4x302TouchscreenSlot != null)
{
Debug.LogMessage(LogEventLevel.Information, this, "Using MPC4x302TouchscreenSlot");
_touchpanel = _processor.MPC4x302TouchscreenSlot;
}
else
{
Debug.LogMessage(LogEventLevel.Error, this, "Failed to find MPC4 Touchpanel Controller with key {0}, check configuration", Key);
return false;
}
if (_touchpanel.Registerable)
{
var registrationResponse = _touchpanel.RegisterWithLogging(Key);
Debug.LogMessage(LogEventLevel.Information, this, "touchpanel registration response: {0}", registrationResponse);
}
_touchpanel.BaseEvent += Touchpanel_BaseEvent;
_touchpanel.ButtonStateChange += Touchpanel_ButtonStateChange;
_touchpanel.PanelStateChange += Touchpanel_PanelStateChange;
foreach (var button in _buttons)
{
var buttonKey = button.Key.ToLower();
var buttonConfig = button.Value;
InitializeButton(buttonKey, buttonConfig);
InitializeButtonFeedback(buttonKey, buttonConfig);
}
ListButtons();
return _touchpanel.Registered;
}
/// <summary>
/// Enables/disables buttons based on event type configuration
/// </summary>
/// <param name="key"></param>
/// <param name="config"></param>
public void InitializeButton(string key, KeypadButton config)
{
if (config == null)
{
Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' config is null, unable to initialize", key);
return;
}
TryParseInt(key, out int buttonNumber);
var buttonEventTypes = config.EventTypes;
BoolOutputSig enabledFb = null;
BoolOutputSig disabledFb = null;
switch (key)
{
case ("power"):
{
if (buttonEventTypes == null || buttonEventTypes.Keys == null)
_touchpanel.DisablePowerButton();
else
_touchpanel.EnablePowerButton();
enabledFb = _touchpanel.PowerButtonEnabledFeedBack;
disabledFb = _touchpanel.PowerButtonDisabledFeedBack;
break;
}
case ("mute"):
{
if (buttonEventTypes == null || buttonEventTypes.Keys == null)
_touchpanel.DisableMuteButton();
else
_touchpanel.EnableMuteButton();
enabledFb = _touchpanel.MuteButtonEnabledFeedBack;
disabledFb = _touchpanel.MuteButtonDisabledFeedBack;
break;
}
default:
{
if (buttonNumber == 0 || buttonNumber > 9)
break;
if (buttonEventTypes == null || buttonEventTypes.Keys == null)
_touchpanel.DisableNumericalButton((uint)buttonNumber);
else
_touchpanel.EnableNumericalButton((uint)buttonNumber);
if (_touchpanel.NumericalButtonEnabledFeedBack != null)
enabledFb = _touchpanel.NumericalButtonEnabledFeedBack[(uint)buttonNumber];
if (_touchpanel.NumericalButtonDisabledFeedBack != null)
disabledFb = _touchpanel.NumericalButtonDisabledFeedBack[(uint)buttonNumber];
break;
}
}
Debug.LogMessage(LogEventLevel.Information, this, "InitializeButton: key-'{0}' enabledFb-'{1}', disabledFb-'{2}'",
key, enabledFb ?? (object)"null", disabledFb ?? (object)"null");
}
/// <summary>
/// Links button feedback if configured
/// </summary>
/// <param name="key"></param>
/// <param name="config"></param>
public void InitializeButtonFeedback(string key, KeypadButton config)
{
if (config == null)
{
Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' config is null, skipping.", key);
return;
}
TryParseInt(key, out int buttonNumber);
// Link up the button feedbacks to the specified device feedback
var buttonFeedback = config.Feedback;
if (buttonFeedback == null || string.IsNullOrEmpty(buttonFeedback.DeviceKey))
{
Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedback not configured, skipping.",
key);
return;
}
Feedback deviceFeedback;
try
{
if (!(DeviceManager.GetDeviceForKey(buttonFeedback.DeviceKey) is Device device))
{
Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedback deviceKey '{1}' not found.",
key, buttonFeedback.DeviceKey);
return;
}
deviceFeedback = device.GetFeedbackProperty(buttonFeedback.FeedbackName);
if (deviceFeedback == null)
{
Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedbackName property '{1}' not found.",
key, buttonFeedback.FeedbackName);
return;
}
}
catch (Exception ex)
{
Debug.LogMessage(LogEventLevel.Debug, this, "InitializeButtonFeedback (button '{1}', deviceKey '{2}') Exception Message: {0}",
ex.Message, key, buttonFeedback.DeviceKey);
Debug.LogMessage(LogEventLevel.Verbose, this, "InitializeButtonFeedback (button '{1}', deviceKey '{2}') Exception StackTrace: {0}",
ex.StackTrace, key, buttonFeedback.DeviceKey);
if (ex.InnerException != null) Debug.LogMessage(LogEventLevel.Verbose, this, "InitializeButtonFeedback (button '{1}', deviceKey '{2}') InnerException: {0}",
ex.InnerException, key, buttonFeedback.DeviceKey);
return;
}
var boolFeedback = deviceFeedback as BoolFeedback;
switch (key)
{
case ("power"):
{
boolFeedback?.LinkCrestronFeedback(_touchpanel.FeedbackPower);
break;
}
case ("volumeup"):
case ("volumedown"):
case ("volumefeedback"):
{
if (deviceFeedback is IntFeedback intFeedback)
{
var volumeFeedback = intFeedback;
volumeFeedback.LinkInputSig(_touchpanel.VolumeBargraph);
}
break;
}
case ("mute"):
{
boolFeedback?.LinkCrestronFeedback(_touchpanel.FeedbackMute);
break;
}
default:
{
boolFeedback?.LinkCrestronFeedback(_touchpanel.Feedbacks[(uint)buttonNumber]);
break;
}
}
}
/// <summary>
/// Try parse int helper method
/// </summary>
/// <param name="str"></param>
/// <param name="result"></param>
/// <returns></returns>
public bool TryParseInt(string str, out int result)
{
try
{
result = int.Parse(str);
return true;
}
catch
{
result = 0;
return false;
}
}
private void Touchpanel_BaseEvent(GenericBase device, BaseEventArgs args)
{
Debug.LogMessage(LogEventLevel.Debug, this, "BaseEvent: eventId-'{0}', index-'{1}'", args.EventId, args.Index);
}
private void Touchpanel_ButtonStateChange(GenericBase device, Crestron.SimplSharpPro.DeviceSupport.ButtonEventArgs args)
{
Debug.LogMessage(LogEventLevel.Debug, this, "ButtonStateChange: buttonNumber-'{0}' buttonName-'{1}', buttonState-'{2}'", args.Button.Number, args.Button.Name, args.NewButtonState);
var type = args.NewButtonState.ToString();
if (_buttons.ContainsKey(args.Button.Number.ToString(CultureInfo.InvariantCulture)))
{
Press(args.Button.Number.ToString(CultureInfo.InvariantCulture), type);
}
else if (_buttons.ContainsKey(args.Button.Name.ToString()))
{
Press(args.Button.Name.ToString(), type);
}
}
private void Touchpanel_PanelStateChange(GenericBase device, BaseEventArgs args)
{
Debug.LogMessage(LogEventLevel.Debug, this, "PanelStateChange: eventId-'{0}', index-'{1}'", args.EventId, args.Index);
}
/// <summary>
/// Runs the function associated with this button/type. One of the following strings:
/// Pressed, Released, Tapped, DoubleTapped, Held, HeldReleased
/// </summary>
/// <param name="buttonKey"></param>
/// <param name="type"></param>
public void Press(string buttonKey, string type)
{
this.LogVerbose("Press: buttonKey-'{buttonKey}', type-'{type}'", buttonKey, type);
if (!_buttons.ContainsKey(buttonKey)) return;
var button = _buttons[buttonKey];
if (!button.EventTypes.ContainsKey(type)) return;
foreach (var eventType in button.EventTypes[type]) DeviceJsonApi.DoDeviceAction(eventType);
}
/// <summary>
/// ListButtons method
/// </summary>
public void ListButtons()
{
this.LogVerbose("MPC4 Controller {0} - Available Buttons", Key);
foreach (var button in _buttons)
{
this.LogVerbose("Key: {key}", button.Key);
}
}
}
}

View file

@ -25,28 +25,11 @@ 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, ICommunicationMonitor public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme
{ {
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>
@ -145,11 +128,6 @@ 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);
@ -388,8 +366,6 @@ 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);
@ -417,17 +393,6 @@ 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>
@ -554,8 +519,15 @@ namespace PepperDash.Essentials.Touchpanel
return false; return false;
}) ? csIpAddress.ToString() : processorIp; }) ? csIpAddress.ToString() : processorIp;
// replace the host but preserve whatever scheme (http/https) is already present in the URL var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app\?token=.+$");
var updatedUrl = Regex.Replace(url, @"^(https?)://[^:/]+", $"$1://{ip}"); 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}");
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl); this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
@ -768,7 +740,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", "mctsw1080", "mcts1080", "mcxpanel", "mcdge1000" }; TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "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>>
@ -793,8 +765,6 @@ 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,16 +127,6 @@ 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>
@ -144,8 +134,7 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
get get
{ {
return string.Format("{0}://{1}:{2}{3}?token=", return string.Format("http://{0}:{1}{2}?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);
@ -284,7 +273,7 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
base.Initialize(); base.Initialize();
_server = new HttpServer(Port, _parent.Config.DirectServer.Secure); _server = new HttpServer(Port, false);
_server.OnGet += Server_OnGet; _server.OnGet += Server_OnGet;
@ -302,7 +291,7 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
ClientCertificateRequired = false, ClientCertificateRequired = false,
CheckCertificateRevocation = false, CheckCertificateRevocation = false,
EnabledSslProtocols = SslProtocols.Tls12 EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11
}; };
} }
@ -414,11 +403,11 @@ namespace PepperDash.Essentials.WebSocketServer
ip = csIpAddress.ToString(); ip = csIpAddress.ToString();
} }
var appUrl = $"{HttpScheme}://{ip}:{Port}/mc/app?token={touchpanel.Key}"; var appUrl = $"http://{ip}:{_parent.Config.DirectServer.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(appUrl); touchpanel.Touchpanel.SetAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
} }
} }
@ -498,7 +487,7 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
var config = new MobileControlApplicationConfig var config = new MobileControlApplicationConfig
{ {
ApiPath = string.Format("{0}://{1}:{2}/mc/api", HttpScheme, processorIp, Port), ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.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,
@ -1109,7 +1098,6 @@ 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"),
@ -1162,11 +1150,6 @@ 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;
@ -1230,7 +1213,8 @@ 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 wsUrl = $"{WsScheme}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}"; var wsProtocol = "ws";
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
@ -1242,8 +1226,7 @@ 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("{0}://{1}:{2}/mc/app", UserAppUrl = string.Format("http://{0}:{1}/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,
@ -1268,8 +1251,6 @@ 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

@ -436,6 +436,25 @@ namespace PepperDash.Essentials
"WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available", "WARNING: Config file defines processor type as '{deviceType:l}' but actual processor is '{processorType:l}'! Some ports may not be available",
devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper()); devConf.Type.ToUpper(), Global.ControlSystem.ControllerPrompt.ToUpper());
// Check if the processor is an MPC4 model
if (prompt.IndexOf("mpc4", StringComparison.OrdinalIgnoreCase) > -1)
{
Debug.LogMessage(LogEventLevel.Information, "MPC4 processor type detected. Adding Mpc4TouchpanelController.");
var butToken = devConf.Properties["buttons"];
if (butToken == null)
{
Debug.LogMessage(LogEventLevel.Warning,
"Error: Unable to deserialize buttons collection for device: {deviceKey}", devConf.Key);
continue;
}
var buttons = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Collections.Generic.Dictionary<string, Core.Touchpanels.KeypadButton>>(butToken.ToString());
var tpController = new Core.Touchpanels.Mpc4TouchpanelController(
string.Format("{0}-keypadButtons", devConf.Key), devConf.Name, Global.ControlSystem, buttons);
DeviceManager.AddDevice(tpController);
}
continue; continue;
} }