Compare commits

..

18 Commits

Author SHA1 Message Date
Neil Dorin
c55de61da9 fix: enhance COM port registration logging and update GenericSink class for input switching 2025-11-05 14:43:42 -07:00
Andrew Welker
42444ede0a Merge pull request #1354 from PepperDash/comport-controller-updates
fix: improve logging for COM port registration and configuration
2025-11-04 10:55:44 -05:00
jkdevito
3d50f5f5ac fix: improve logging for COM port registration and configuration 2025-11-04 09:37:14 -06:00
Nick Genovese
11d62aebe1 Merge pull request #1353 from PepperDash/mc-subscription-concurrency
fix: make subscriber functionality thread-safe
2025-11-03 17:26:31 -04:00
Andrew Welker
edc10a9c2a fix: make subscriberIds private & check for add failure 2025-11-03 15:19:17 -06:00
Andrew Welker
9be5823956 Merge branch 'main' into mc-subscription-concurrency 2025-11-03 15:18:45 -06:00
Andrew Welker
35371dde22 fix: make subscriber functionality thread-safe 2025-11-03 15:13:48 -06:00
Jason DeVito
d3ceb4d7e7 Merge pull request #1352 from PepperDash/theme-saving
fix: use correct overload for PostStatusMessage
2025-11-03 15:08:28 -06:00
Andrew Welker
a782b57100 fix: use correct overload for PostStatusMessage 2025-11-03 14:00:51 -06:00
Neil Dorin
1360de599f Merge pull request #1351 from PepperDash/stream-debugging
fix: centralize debug printing into extension methods
2025-11-03 12:19:05 -05:00
Andrew Welker
fd95f5fed1 docs: update XML docs
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-03 11:11:00 -06:00
Andrew Welker
9426dff5df fix: copilot suggestions from PR review 2025-11-03 11:02:39 -06:00
Andrew Welker
0d083e63c6 Merge commit '314570d6c3d78c7a92a362f3ec3a4a06bdbebd28' into stream-debugging 2025-11-03 10:56:53 -06:00
Andrew Welker
6ed7c96ec7 fix: centralize debug printing into extension methods
Stream debugging now uses CrestronConsole instead of debug methods, so that the debug statements will be printed regardless of console debug level. This also means that comm debug statements will NOT be in the Crestron Error log or in the files created by the logging system
2025-11-03 10:53:21 -06:00
Andrew Welker
314570d6c3 fix: change number of retained files to 7 instead of 30 for processors 2025-10-31 13:11:50 -05:00
Andrew Welker
ff609dfecd fix: add config option to turn echo off for SSH
In addition, removed CTimer in favor of System.Threading.Timers Timer in the SSH class, and modified the class to be thread-safe.
2025-10-31 09:01:54 -05:00
Andrew Welker
666be5ae59 Merge pull request #1345 from PepperDash/mc-touchpanel-key
mc touchpanel key
2025-10-30 17:11:15 -04:00
Andrew Welker
bfc9b7e7fa fix: logging & ternary changes 2025-10-30 16:07:26 -05:00
20 changed files with 746 additions and 613 deletions

View File

@@ -0,0 +1,43 @@
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace PepperDash.Core
{
/// <summary>
/// Helper class for formatting communication text and byte data for debugging purposes.
/// </summary>
public class ComTextHelper
{
/// <summary>
/// Gets escaped text for a byte array
/// </summary>
/// <param name="bytes"></param>
/// <returns>string with all bytes escaped</returns>
public static string GetEscapedText(byte[] bytes)
{
return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
}
/// <summary>
/// Gets escaped text for a string
/// </summary>
/// <param name="text"></param>
/// <returns>string with all bytes escaped</returns>
public static string GetEscapedText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
return string.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
}
/// <summary>
/// Gets debug text for a string
/// </summary>
/// <param name="text"></param>
/// <returns>string with all non-printable characters escaped</returns>
public static string GetDebugText(string text)
{
return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value));
}
}
}

View File

@@ -1,5 +1,4 @@
using System; using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
@@ -136,51 +135,4 @@ namespace PepperDash.Core
DebugExpiryPeriod = null; DebugExpiryPeriod = null;
} }
} }
/// <summary>
/// The available settings for stream debugging
/// </summary>
[Flags]
/// <summary>
/// Enumeration of eStreamDebuggingSetting values
/// </summary>
public enum eStreamDebuggingSetting
{
/// <summary>
/// Debug off
/// </summary>
Off = 0,
/// <summary>
/// Debug received data
/// </summary>
Rx = 1,
/// <summary>
/// Debug transmitted data
/// </summary>
Tx = 2,
/// <summary>
/// Debug both received and transmitted data
/// </summary>
Both = Rx | Tx
}
/// <summary>
/// The available settings for stream debugging response types
/// </summary>
[Flags]
public enum eStreamDebuggingDataTypeSettings
{
/// <summary>
/// Debug data in byte format
/// </summary>
Bytes = 0,
/// <summary>
/// Debug data in text format
/// </summary>
Text = 1,
/// <summary>
/// Debug data in both byte and text formats
/// </summary>
Both = Bytes | Text,
}
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using Crestron.SimplSharp; using Crestron.SimplSharp;
@@ -11,11 +12,12 @@ using Renci.SshNet.Common;
namespace PepperDash.Core namespace PepperDash.Core
{ {
/// <summary> /// <summary>
/// /// SSH Client
/// </summary> /// </summary>
public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{ {
private const string SPlusKey = "Uninitialized SshClient"; private const string SPlusKey = "Uninitialized SshClient";
/// <summary> /// <summary>
/// Object to enable stream debugging /// Object to enable stream debugging
/// </summary> /// </summary>
@@ -36,11 +38,6 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange; public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
/////
///// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
/// <summary> /// <summary>
/// Gets or sets the Hostname /// Gets or sets the Hostname
/// </summary> /// </summary>
@@ -67,7 +64,7 @@ namespace PepperDash.Core
public bool IsConnected public bool IsConnected
{ {
// returns false if no client or not connected // returns false if no client or not connected
get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } get { return client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
} }
/// <summary> /// <summary>
@@ -83,16 +80,26 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public SocketStatus ClientStatus public SocketStatus ClientStatus
{ {
get { return _ClientStatus; } get { lock (_stateLock) { return _ClientStatus; } }
private set private set
{ {
if (_ClientStatus == value) bool shouldFireEvent = false;
return; lock (_stateLock)
{
if (_ClientStatus != value)
{
_ClientStatus = value; _ClientStatus = value;
shouldFireEvent = true;
}
}
// Fire event outside lock to avoid deadlock
if (shouldFireEvent)
OnConnectionChange(); OnConnectionChange();
} }
} }
SocketStatus _ClientStatus;
private SocketStatus _ClientStatus;
private bool _ConnectEnabled;
/// <summary> /// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
@@ -100,7 +107,7 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public ushort UStatus public ushort UStatus
{ {
get { return (ushort)_ClientStatus; } get { lock (_stateLock) { return (ushort)_ClientStatus; } }
} }
/// <summary> /// <summary>
@@ -111,7 +118,11 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Will be set and unset by connect and disconnect only /// Will be set and unset by connect and disconnect only
/// </summary> /// </summary>
public bool ConnectEnabled { get; private set; } public bool ConnectEnabled
{
get { lock (_stateLock) { return _ConnectEnabled; } }
private set { lock (_stateLock) { _ConnectEnabled = value; } }
}
/// <summary> /// <summary>
/// S+ helper for AutoReconnect /// S+ helper for AutoReconnect
@@ -127,17 +138,25 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public int AutoReconnectIntervalMs { get; set; } public int AutoReconnectIntervalMs { get; set; }
SshClient Client; private SshClient client;
ShellStream TheStream; private ShellStream shellStream;
CTimer ReconnectTimer; private readonly Timer reconnectTimer;
//Lock object to prevent simulatneous connect/disconnect operations //Lock object to prevent simulatneous connect/disconnect operations
//private CCriticalSection connectLock = new CCriticalSection(); //private CCriticalSection connectLock = new CCriticalSection();
private SemaphoreSlim connectLock = new SemaphoreSlim(1); private readonly SemaphoreSlim connectLock = new SemaphoreSlim(1);
private bool DisconnectLogged = false; // Thread-safety lock for state changes
private readonly object _stateLock = new object();
private bool disconnectLogged = false;
/// <summary>
/// When true, turns off echo for the SSH session
/// </summary>
public bool DisableEcho { get; set; }
/// <summary> /// <summary>
/// Typical constructor. /// Typical constructor.
@@ -154,13 +173,13 @@ namespace PepperDash.Core
Password = password; Password = password;
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o => reconnectTimer = new Timer(o =>
{ {
if (ConnectEnabled) if (ConnectEnabled) // Now thread-safe property access
{ {
Connect(); Connect();
} }
}, System.Threading.Timeout.Infinite); }, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
} }
/// <summary> /// <summary>
@@ -172,23 +191,23 @@ namespace PepperDash.Core
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o => reconnectTimer = new Timer(o =>
{ {
if (ConnectEnabled) if (ConnectEnabled) // Now thread-safe property access
{ {
Connect(); Connect();
} }
}, System.Threading.Timeout.Infinite); }, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
} }
/// <summary> /// <summary>
/// Handles closing this up when the program shuts down /// Handles closing this up when the program shuts down
/// </summary> /// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{ {
if (programEventType == eProgramStatusEventType.Stopping) if (programEventType == eProgramStatusEventType.Stopping)
{ {
if (Client != null) if (client != null)
{ {
this.LogDebug("Program stopping. Closing connection"); this.LogDebug("Program stopping. Closing connection");
Disconnect(); Disconnect();
@@ -223,10 +242,10 @@ namespace PepperDash.Core
this.LogDebug("Attempting connect"); this.LogDebug("Attempting connect");
// Cancel reconnect if running. // Cancel reconnect if running.
ReconnectTimer?.Stop(); StopReconnectTimer();
// Cleanup the old client if it already exists // Cleanup the old client if it already exists
if (Client != null) if (client != null)
{ {
this.LogDebug("Cleaning up disconnected client"); this.LogDebug("Cleaning up disconnected client");
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
@@ -239,29 +258,36 @@ namespace PepperDash.Core
this.LogDebug("Creating new SshClient"); this.LogDebug("Creating new SshClient");
ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth); ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth);
Client = new SshClient(connectionInfo); client = new SshClient(connectionInfo);
Client.ErrorOccurred += Client_ErrorOccurred; client.ErrorOccurred += Client_ErrorOccurred;
//Attempt to connect //Attempt to connect
ClientStatus = SocketStatus.SOCKET_STATUS_WAITING; ClientStatus = SocketStatus.SOCKET_STATUS_WAITING;
try try
{ {
Client.Connect(); client.Connect();
TheStream = Client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534);
if (TheStream.DataAvailable) var modes = new Dictionary<TerminalModes, uint>();
if (DisableEcho)
{
modes.Add(TerminalModes.ECHO, 0);
}
shellStream = client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534, modes);
if (shellStream.DataAvailable)
{ {
// empty the buffer if there is data // empty the buffer if there is data
string str = TheStream.Read(); shellStream.Read();
} }
TheStream.DataReceived += Stream_DataReceived; shellStream.DataReceived += Stream_DataReceived;
this.LogInformation("Connected"); this.LogInformation("Connected");
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED; ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
DisconnectLogged = false; disconnectLogged = false;
} }
catch (SshConnectionException e) catch (SshConnectionException e)
{ {
var ie = e.InnerException; // The details are inside!! var ie = e.InnerException; // The details are inside!!
var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error;
if (ie is SocketException) if (ie is SocketException)
{ {
@@ -286,37 +312,36 @@ namespace PepperDash.Core
this.LogVerbose(ie, "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) if (AutoReconnect)
{ {
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs); StartReconnectTimer();
} }
} }
catch (SshOperationTimeoutException ex) catch (SshOperationTimeoutException ex)
{ {
this.LogWarning("Connection attempt timed out: {message}", ex.Message); this.LogWarning("Connection attempt timed out: {message}", ex.Message);
DisconnectLogged = true; disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED); KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect) if (AutoReconnect)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs); StartReconnectTimer();
} }
} }
catch (Exception e) catch (Exception e)
{ {
var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error;
this.LogError("Unhandled exception on connect: {error}", e.Message); this.LogError("Unhandled exception on connect: {error}", e.Message);
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)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs); StartReconnectTimer();
} }
} }
} }
@@ -334,11 +359,7 @@ namespace PepperDash.Core
{ {
ConnectEnabled = false; ConnectEnabled = false;
// Stop trying reconnects, if we are // Stop trying reconnects, if we are
if (ReconnectTimer != null) StopReconnectTimer();
{
ReconnectTimer.Stop();
// ReconnectTimer = null;
}
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY); KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
} }
@@ -352,12 +373,12 @@ namespace PepperDash.Core
try try
{ {
if (Client != null) if (client != null)
{ {
Client.ErrorOccurred -= Client_ErrorOccurred; client.ErrorOccurred -= Client_ErrorOccurred;
Client.Disconnect(); client.Disconnect();
Client.Dispose(); client.Dispose();
Client = null; client = null;
ClientStatus = status; ClientStatus = status;
this.LogDebug("Disconnected"); this.LogDebug("Disconnected");
} }
@@ -371,16 +392,16 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Kills the stream /// Kills the stream
/// </summary> /// </summary>
void KillStream() private void KillStream()
{ {
try try
{ {
if (TheStream != null) if (shellStream != null)
{ {
TheStream.DataReceived -= Stream_DataReceived; shellStream.DataReceived -= Stream_DataReceived;
TheStream.Close(); shellStream.Close();
TheStream.Dispose(); shellStream.Dispose();
TheStream = null; shellStream = null;
this.LogDebug("Disconnected stream"); this.LogDebug("Disconnected stream");
} }
} }
@@ -393,7 +414,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Handles the keyboard interactive authentication, should it be required. /// Handles the keyboard interactive authentication, should it be required.
/// </summary> /// </summary>
void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e) private void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
{ {
foreach (AuthenticationPrompt prompt in e.Prompts) foreach (AuthenticationPrompt prompt in e.Prompts)
if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1) if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
@@ -403,7 +424,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Handler for data receive on ShellStream. Passes data across to queue for line parsing. /// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
/// </summary> /// </summary>
void Stream_DataReceived(object sender, ShellDataEventArgs e) private void Stream_DataReceived(object sender, ShellDataEventArgs e)
{ {
if (((ShellStream)sender).Length <= 0L) if (((ShellStream)sender).Length <= 0L)
{ {
@@ -416,18 +437,14 @@ namespace PepperDash.Core
if (bytesHandler != null) if (bytesHandler != null)
{ {
var bytes = Encoding.UTF8.GetBytes(response); var bytes = Encoding.UTF8.GetBytes(response);
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedBytes(bytes);
{
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
} }
var textHandler = TextReceived; var textHandler = TextReceived;
if (textHandler != null) if (textHandler != null)
{ {
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedText(response);
this.LogInformation("Received: '{0}'", ComTextHelper.GetDebugText(response));
textHandler(this, new GenericCommMethodReceiveTextArgs(response)); textHandler(this, new GenericCommMethodReceiveTextArgs(response));
} }
@@ -439,7 +456,7 @@ namespace PepperDash.Core
/// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange /// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
/// event /// event
/// </summary> /// </summary>
void Client_ErrorOccurred(object sender, ExceptionEventArgs e) private void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
{ {
CrestronInvoke.BeginInvoke(o => CrestronInvoke.BeginInvoke(o =>
{ {
@@ -459,7 +476,7 @@ namespace PepperDash.Core
if (AutoReconnect && ConnectEnabled) if (AutoReconnect && ConnectEnabled)
{ {
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs); this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs); StartReconnectTimer();
} }
}); });
} }
@@ -467,7 +484,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Helper for ConnectionChange event /// Helper for ConnectionChange event
/// </summary> /// </summary>
void OnConnectionChange() private void OnConnectionChange()
{ {
ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this)); ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this));
} }
@@ -482,16 +499,12 @@ namespace PepperDash.Core
{ {
try try
{ {
if (Client != null && TheStream != null && IsConnected) if (client != null && shellStream != null && IsConnected)
{ {
if (StreamDebugging.TxStreamDebuggingIsEnabled) this.PrintSentText(text);
this.LogInformation(
"Sending {length} characters of text: '{text}'",
text.Length,
ComTextHelper.GetDebugText(text));
TheStream.Write(text); shellStream.Write(text);
TheStream.Flush(); shellStream.Flush();
} }
else else
{ {
@@ -503,7 +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);
ReconnectTimer.Reset(); StartReconnectTimer();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -519,13 +532,12 @@ namespace PepperDash.Core
{ {
try try
{ {
if (Client != null && TheStream != null && IsConnected) if (client != null && shellStream != null && IsConnected)
{ {
if (StreamDebugging.TxStreamDebuggingIsEnabled) this.PrintSentBytes(bytes);
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
TheStream.Write(bytes, 0, bytes.Length); shellStream.Write(bytes, 0, bytes.Length);
TheStream.Flush(); shellStream.Flush();
} }
else else
{ {
@@ -537,7 +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);
ReconnectTimer.Reset(); StartReconnectTimer();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -546,6 +558,83 @@ namespace PepperDash.Core
} }
#endregion #endregion
/// <summary>
/// Safely starts the reconnect timer with exception handling
/// </summary>
private void StartReconnectTimer()
{
try
{
reconnectTimer?.Change(AutoReconnectIntervalMs, System.Threading.Timeout.Infinite);
}
catch (ObjectDisposedException)
{
// Timer was disposed, ignore
this.LogDebug("Attempted to start timer but it was already disposed");
}
}
/// <summary>
/// Safely stops the reconnect timer with exception handling
/// </summary>
private void StopReconnectTimer()
{
try
{
reconnectTimer?.Change(System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
}
catch (ObjectDisposedException)
{
// Timer was disposed, ignore
this.LogDebug("Attempted to stop timer but it was already disposed");
}
}
/// <summary>
/// Deactivate method - properly dispose of resources
/// </summary>
public override bool Deactivate()
{
try
{
this.LogDebug("Deactivating SSH client - disposing resources");
// Stop trying reconnects
ConnectEnabled = false;
StopReconnectTimer();
// Disconnect and cleanup client
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
// Dispose timer
try
{
reconnectTimer?.Dispose();
}
catch (ObjectDisposedException)
{
// Already disposed, ignore
}
// Dispose semaphore
try
{
connectLock?.Dispose();
}
catch (ObjectDisposedException)
{
// Already disposed, ignore
}
return base.Deactivate();
}
catch (Exception ex)
{
this.LogException(ex, "Error during SSH client deactivation");
return false;
}
}
} }
//***************************************************************************************************** //*****************************************************************************************************

View File

@@ -426,10 +426,7 @@ namespace PepperDash.Core
var bytesHandler = BytesReceived; var bytesHandler = BytesReceived;
if (bytesHandler != null) if (bytesHandler != null)
{ {
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedBytes(bytes);
{
Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
} }
var textHandler = TextReceived; var textHandler = TextReceived;
@@ -437,10 +434,7 @@ namespace PepperDash.Core
{ {
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedText(str);
{
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
}
textHandler(this, new GenericCommMethodReceiveTextArgs(str)); textHandler(this, new GenericCommMethodReceiveTextArgs(str));
} }
@@ -456,8 +450,7 @@ namespace PepperDash.Core
{ {
var bytes = Encoding.GetEncoding(28591).GetBytes(text); var bytes = Encoding.GetEncoding(28591).GetBytes(text);
// Check debug level before processing byte array // Check debug level before processing byte array
if (StreamDebugging.TxStreamDebuggingIsEnabled) this.PrintSentText(text);
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
if (_client != null) if (_client != null)
_client.SendData(bytes, bytes.Length); _client.SendData(bytes, bytes.Length);
} }
@@ -484,8 +477,7 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
if (StreamDebugging.TxStreamDebuggingIsEnabled) this.PrintSentBytes(bytes);
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (_client != null) if (_client != null)
_client.SendData(bytes, bytes.Length); _client.SendData(bytes, bytes.Length);
} }
@@ -555,6 +547,12 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public int AutoReconnectIntervalMs { get; set; } public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// When true, turns off echo for the SSH session
/// </summary>
[JsonProperty("disableSshEcho")]
public bool DisableSshEcho { get; set; }
/// <summary> /// <summary>
/// Default constructor /// Default constructor
/// </summary> /// </summary>
@@ -565,8 +563,7 @@ namespace PepperDash.Core
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
Username = ""; Username = "";
Password = ""; Password = "";
DisableSshEcho = false;
} }
} }
} }

View File

@@ -281,17 +281,13 @@ namespace PepperDash.Core
var bytesHandler = BytesReceived; var bytesHandler = BytesReceived;
if (bytesHandler != null) if (bytesHandler != null)
{ {
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedBytes(bytes);
{
Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
} }
var textHandler = TextReceived; var textHandler = TextReceived;
if (textHandler != null) if (textHandler != null)
{ {
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedText(str);
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
textHandler(this, new GenericCommMethodReceiveTextArgs(str)); textHandler(this, new GenericCommMethodReceiveTextArgs(str));
} }
} }
@@ -318,8 +314,7 @@ namespace PepperDash.Core
if (IsConnected && Server != null) if (IsConnected && Server != null)
{ {
if (StreamDebugging.TxStreamDebuggingIsEnabled) this.PrintSentText(text);
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
Server.SendData(bytes, bytes.Length); Server.SendData(bytes, bytes.Length);
} }
@@ -334,8 +329,7 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public void SendBytes(byte[] bytes) public void SendBytes(byte[] bytes)
{ {
if (StreamDebugging.TxStreamDebuggingIsEnabled) this.PrintSentBytes(bytes);
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (IsConnected && Server != null) if (IsConnected && Server != null)
Server.SendData(bytes, bytes.Length); Server.SendData(bytes, bytes.Length);

View File

@@ -0,0 +1,69 @@
using System;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Extension methods for stream debugging
/// </summary>
public static class StreamDebuggingExtensions
{
private static readonly string app = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? $"App {InitialParametersClass.ApplicationNumber}" : $"{InitialParametersClass.RoomId}";
/// <summary>
/// Print the sent bytes to the console
/// </summary>
/// <param name="comms">comms device</param>
/// <param name="bytes">bytes to print</param>
public static void PrintSentBytes(this IStreamDebugging comms, byte[] bytes)
{
if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return;
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'");
}
/// <summary>
/// Print the received bytes to the console
/// </summary>
/// <param name="comms">comms device</param>
/// <param name="bytes">bytes to print</param>
public static void PrintReceivedBytes(this IStreamDebugging comms, byte[] bytes)
{
if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return;
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received {bytes.Length} bytes: '{ComTextHelper.GetEscapedText(bytes)}'");
}
/// <summary>
/// Print the sent text to the console
/// </summary>
/// <param name="comms">comms device</param>
/// <param name="text">text to print</param>
public static void PrintSentText(this IStreamDebugging comms, string text)
{
if (!comms.StreamDebugging.TxStreamDebuggingIsEnabled) return;
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Sending Text: '{ComTextHelper.GetDebugText(text)}'");
}
/// <summary>
/// Print the received text to the console
/// </summary>
/// <param name="comms">comms device</param>
/// <param name="text">text to print</param>
public static void PrintReceivedText(this IStreamDebugging comms, string text)
{
if (!comms.StreamDebugging.RxStreamDebuggingIsEnabled) return;
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
CrestronConsole.PrintLine($"[{timestamp}][{app}][{comms.Key}] Received Text: '{ComTextHelper.GetDebugText(text)}'");
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
namespace PepperDash.Core
{
/// <summary>
/// The available settings for stream debugging data format types
/// </summary>
[Flags]
public enum eStreamDebuggingDataTypeSettings
{
/// <summary>
/// Debug data in byte format
/// </summary>
Bytes = 0,
/// <summary>
/// Debug data in text format
/// </summary>
Text = 1,
/// <summary>
/// Debug data in both byte and text formats
/// </summary>
Both = Bytes | Text
}
}

View File

@@ -0,0 +1,28 @@
using System;
namespace PepperDash.Core
{
/// <summary>
/// The available settings for stream debugging
/// </summary>
[Flags]
public enum eStreamDebuggingSetting
{
/// <summary>
/// Debug off
/// </summary>
Off = 0,
/// <summary>
/// Debug received data
/// </summary>
Rx = 1,
/// <summary>
/// Debug transmitted data
/// </summary>
Tx = 2,
/// <summary>
/// Debug both received and transmitted data
/// </summary>
Both = Rx | Tx
}
}

View File

@@ -1,10 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp; using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets; using Crestron.SimplSharp.CrestronSockets;
using System.Text.RegularExpressions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace PepperDash.Core namespace PepperDash.Core
@@ -67,7 +64,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// Represents a device with stream debugging capablities /// Represents a device with stream debugging capablities
/// </summary> /// </summary>
public interface IStreamDebugging public interface IStreamDebugging : IKeyed
{ {
/// <summary> /// <summary>
/// Object to enable stream debugging /// Object to enable stream debugging
@@ -205,49 +202,4 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public GenericCommMethodReceiveTextArgs() { } public GenericCommMethodReceiveTextArgs() { }
} }
/// <summary>
///
/// </summary>
public class ComTextHelper
{
/// <summary>
/// Gets escaped text for a byte array
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string GetEscapedText(byte[] bytes)
{
return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
}
/// <summary>
/// Gets escaped text for a string
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
/// <summary>
/// GetEscapedText method
/// </summary>
public static string GetEscapedText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
}
/// <summary>
/// Gets debug text for a string
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
/// <summary>
/// GetDebugText method
/// </summary>
public static string GetDebugText(string text)
{
return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value));
}
}
} }

View File

@@ -168,7 +168,7 @@ namespace PepperDash.Core
.WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath, .WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath,
rollingInterval: RollingInterval.Day, rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: LogEventLevel.Debug, restrictedToMinimumLevel: LogEventLevel.Debug,
retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60, retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 7 : 14,
levelSwitch: _fileLogLevelSwitch levelSwitch: _fileLogLevelSwitch
); );
@@ -1081,9 +1081,6 @@ namespace PepperDash.Core
/// Logs to Console when at-level, and all messages to error log /// Logs to Console when at-level, and all messages to error log
/// </summary> /// </summary>
[Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")]
/// <summary>
/// Console method
/// </summary>
public static void Console(uint level, ErrorLogLevel errorLogLevel, public static void Console(uint level, ErrorLogLevel errorLogLevel,
string format, params object[] items) string format, params object[] items)
{ {
@@ -1096,9 +1093,6 @@ namespace PepperDash.Core
/// it will only be written to the log. /// it will only be written to the log.
/// </summary> /// </summary>
[Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")]
/// <summary>
/// ConsoleWithLog method
/// </summary>
public static void ConsoleWithLog(uint level, string format, params object[] items) public static void ConsoleWithLog(uint level, string format, params object[] items)
{ {
LogMessage(level, format, items); LogMessage(level, format, items);

View File

@@ -68,15 +68,13 @@ namespace PepperDash.Essentials.Core
if (bytesHandler != null) if (bytesHandler != null)
{ {
var bytes = Encoding.GetEncoding(28591).GetBytes(s); var bytes = Encoding.GetEncoding(28591).GetBytes(s);
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedBytes(bytes);
Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", ComTextHelper.GetEscapedText(bytes));
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
} }
var textHandler = TextReceived; var textHandler = TextReceived;
if (textHandler != null) if (textHandler != null)
{ {
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedText(s);
Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", s);
textHandler(this, new GenericCommMethodReceiveTextArgs(s)); textHandler(this, new GenericCommMethodReceiveTextArgs(s));
} }
} }
@@ -90,8 +88,7 @@ namespace PepperDash.Essentials.Core
{ {
if (Port == null) if (Port == null)
return; return;
if (StreamDebugging.TxStreamDebuggingIsEnabled) this.PrintSentText(text);
Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} characters of text: '{1}'", text.Length, text);
Port.StreamCec.Send.StringValue = text; Port.StreamCec.Send.StringValue = text;
} }
@@ -103,7 +100,7 @@ namespace PepperDash.Essentials.Core
if (Port == null) if (Port == null)
return; return;
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
if (StreamDebugging.TxStreamDebuggingIsEnabled) 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));
Port.StreamCec.Send.StringValue = text; Port.StreamCec.Send.StringValue = text;
} }

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro; using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.GeneralIO;
using PepperDash.Core; using PepperDash.Core;
using PepperDash.Core.Logging; using PepperDash.Core.Logging;
using Serilog.Events; using Serilog.Events;
@@ -85,61 +87,29 @@ namespace PepperDash.Essentials.Core
{ {
if (Port == null) if (Port == null)
{ {
Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist."); this.LogInformation($"Configured {Port.Parent.GetType().Name}-comport-{Port.ID} for {Key} does not exist.");
return; return;
} }
// TODO [ ] - Remove commented out code once verified working
//if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102)
if (Port.Parent is GenericBase genericDevice && genericDevice.Registerable) if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102)
{ {
//this.LogInformation($"INFO: Attempting to register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID}"); var result = Port.Register();
var result = genericDevice.Register();
if (result != eDeviceRegistrationUnRegistrationResponse.Success) if (result != eDeviceRegistrationUnRegistrationResponse.Success)
{ {
this.LogError($"ERROR: Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})"); this.LogError($"Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})");
return; // false return;
} }
this.LogInformation($"Successfully registered {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})");
} }
var specResult = Port.SetComPortSpec(Spec); var specResult = Port.SetComPortSpec(Spec);
if (specResult != 0) if (specResult != 0)
{ {
this.LogError($"ERROR: Cannot set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); this.LogError($"Cannot set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})");
return; return;
} }
//this.LogInformation($"INFO: Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})"); this.LogInformation($"Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})");
// TODO [ ] - Remove debug logging once verified working
// if (Port.Parent is CenIoCom102)
// {
// Port.PropertyChanged += (s, e) =>
// {
// this.LogInformation($@"RegisterAndConfigureComPort: PropertyChanged Fired >>
// comPort-'{Port.ID}',
// Property Changed-'{e.Property}',
// Value Changed-'{e.Value}',
// deviceName-'{Port.DeviceName}',
// parentDevice-'{Port.ParentDevice}',
// parent-`{Port.Parent}`,
// online-`{Port.IsOnline}`,
// present-`{Port.Present}`,
// supportedBaudRates-'{Port.SupportedBaudRates}'");
// };
// Port.ExtendedInformationChanged += (s, e) =>
// {
// this.LogInformation($@"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >>
// comPort-'{Port.ID}',
// {e.Protocol},
// {e.BaudRate},
// {e.Parity},
// {e.DataBits},
// {e.StopBits},
// HW Handshake-'{e.HardwareHandshakeSetting}',
// SW Handshake-'{e.SoftwareHandshakeSetting}'");
// };
// }
Port.SerialDataReceived += Port_SerialDataReceived; Port.SerialDataReceived += Port_SerialDataReceived;
} }
@@ -165,16 +135,14 @@ namespace PepperDash.Essentials.Core
if (bytesHandler != null) if (bytesHandler != null)
{ {
var bytes = Encoding.GetEncoding(28591).GetBytes(s); var bytes = Encoding.GetEncoding(28591).GetBytes(s);
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedBytes(bytes);
Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", ComTextHelper.GetEscapedText(bytes));
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes)); bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
eventSubscribed = true; eventSubscribed = true;
} }
var textHandler = TextReceived; var textHandler = TextReceived;
if (textHandler != null) if (textHandler != null)
{ {
if (StreamDebugging.RxStreamDebuggingIsEnabled) this.PrintReceivedText(s);
Debug.LogMessage(LogEventLevel.Information, this, "Received: '{0}'", s);
textHandler(this, new GenericCommMethodReceiveTextArgs(s)); textHandler(this, new GenericCommMethodReceiveTextArgs(s));
eventSubscribed = true; eventSubscribed = true;
} }
@@ -201,8 +169,7 @@ namespace PepperDash.Essentials.Core
if (Port == null) if (Port == null)
return; return;
if (StreamDebugging.TxStreamDebuggingIsEnabled) this.PrintSentText(text);
Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} characters of text: '{1}'", text.Length, text);
Port.Send(text); Port.Send(text);
} }
@@ -214,8 +181,7 @@ namespace PepperDash.Essentials.Core
if (Port == null) if (Port == null)
return; return;
var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length); var text = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
if (StreamDebugging.TxStreamDebuggingIsEnabled) this.PrintSentBytes(bytes);
Debug.LogMessage(LogEventLevel.Information, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
Port.Send(text); Port.Send(text);
} }

View File

@@ -64,8 +64,11 @@ namespace PepperDash.Essentials.Core
break; break;
case eControlMethod.Ssh: case eControlMethod.Ssh:
{ {
var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password); var ssh = new GenericSshClient(deviceConfig.Key + "-ssh", c.Address, c.Port, c.Username, c.Password)
ssh.AutoReconnect = c.AutoReconnect; {
AutoReconnect = c.AutoReconnect,
DisableEcho = c.DisableSshEcho
};
if (ssh.AutoReconnect) if (ssh.AutoReconnect)
ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; ssh.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
comm = ssh; comm = ssh;
@@ -73,8 +76,10 @@ namespace PepperDash.Essentials.Core
} }
case eControlMethod.Tcpip: case eControlMethod.Tcpip:
{ {
var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize); var tcp = new GenericTcpIpClient(deviceConfig.Key + "-tcp", c.Address, c.Port, c.BufferSize)
tcp.AutoReconnect = c.AutoReconnect; {
AutoReconnect = c.AutoReconnect
};
if (tcp.AutoReconnect) if (tcp.AutoReconnect)
tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; tcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
comm = tcp; comm = tcp;
@@ -90,8 +95,10 @@ namespace PepperDash.Essentials.Core
break; break;
case eControlMethod.SecureTcpIp: case eControlMethod.SecureTcpIp:
{ {
var secureTcp = new GenericSecureTcpIpClient(deviceConfig.Key + "-secureTcp", c.Address, c.Port, c.BufferSize); var secureTcp = new GenericSecureTcpIpClient(deviceConfig.Key + "-secureTcp", c.Address, c.Port, c.BufferSize)
secureTcp.AutoReconnect = c.AutoReconnect; {
AutoReconnect = c.AutoReconnect
};
if (secureTcp.AutoReconnect) if (secureTcp.AutoReconnect)
secureTcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs; secureTcp.AutoReconnectIntervalMs = c.AutoReconnectIntervalMs;
comm = secureTcp; comm = secureTcp;

View File

@@ -98,7 +98,7 @@ namespace PepperDash.Essentials.Core.Routing
/// <param name="inputPort">The currently selected input port on the destination device.</param> /// <param name="inputPort">The currently selected input port on the destination device.</param>
private void UpdateDestination(IRoutingSinkWithSwitching destination, RoutingInputPort inputPort) private void UpdateDestination(IRoutingSinkWithSwitching destination, RoutingInputPort inputPort)
{ {
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this,destination?.Key, inputPort?.Key); Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this, destination?.Key, inputPort?.Key);
if(inputPort == null) if(inputPort == null)
{ {

View File

@@ -118,15 +118,6 @@ namespace PepperDash.Essentials.Core.UI
/// <param name="roomKey">Room Key for this panel</param> /// <param name="roomKey">Room Key for this panel</param>
protected abstract void SetupPanelDrivers(string roomKey); protected abstract void SetupPanelDrivers(string roomKey);
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
}
/// <summary> /// <summary>
/// Event handler for System Extender Events /// Event handler for System Extender Events
/// </summary> /// </summary>

View File

@@ -9,7 +9,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic
/// <summary> /// <summary>
/// Represents a GenericSink /// Represents a GenericSink
/// </summary> /// </summary>
public class GenericSink : EssentialsDevice, IRoutingSinkWithInputPort public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort
{ {
/// <summary> /// <summary>
/// Initializes a new instance of the GenericSink class /// Initializes a new instance of the GenericSink class
@@ -20,7 +20,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic
{ {
InputPorts = new RoutingPortCollection<RoutingInputPort>(); InputPorts = new RoutingPortCollection<RoutingInputPort>();
var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this); var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this);
InputPorts.Add(inputPort); InputPorts.Add(inputPort);
} }
@@ -66,6 +66,15 @@ namespace PepperDash.Essentials.Devices.Common.Generic
/// Event fired when the current source changes /// Event fired when the current source changes
/// </summary> /// </summary>
public event SourceInfoChangeHandler CurrentSourceChange; public event SourceInfoChangeHandler CurrentSourceChange;
/// <inheritdoc />
public event InputChangedEventHandler InputChanged;
/// <inheritdoc />
public void ExecuteSwitch(object inputSelector)
{
throw new System.NotImplementedException();
}
} }
/// <summary> /// <summary>

View File

@@ -31,7 +31,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <remarks> /// <remarks>
/// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection. /// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection.
/// </remarks> /// </remarks>
protected HashSet<string> SubscriberIds = new HashSet<string>(); private readonly HashSet<string> subscriberIds = new HashSet<string>();
/// <summary>
/// Lock object for thread-safe access to SubscriberIds
/// </summary>
private readonly object _subscriberLock = new object();
private readonly List<string> _deviceInterfaces; private readonly List<string> _deviceInterfaces;
@@ -193,13 +198,14 @@ namespace PepperDash.Essentials.AppServer.Messengers
return; return;
} }
if (SubscriberIds.Any(id => id == clientId)) lock (_subscriberLock)
{
if (!subscriberIds.Add(clientId))
{ {
this.LogVerbose("Client {clientId} already subscribed", clientId); this.LogVerbose("Client {clientId} already subscribed", clientId);
return; return;
} }
}
SubscriberIds.Add(clientId);
this.LogDebug("Client {clientId} subscribed", clientId); this.LogDebug("Client {clientId} subscribed", clientId);
} }
@@ -216,14 +222,22 @@ namespace PepperDash.Essentials.AppServer.Messengers
return; return;
} }
if (!SubscriberIds.Any(i => i == clientId)) bool wasSubscribed;
lock (_subscriberLock)
{
wasSubscribed = subscriberIds.Contains(clientId);
if (wasSubscribed)
{
subscriberIds.Remove(clientId);
}
}
if (!wasSubscribed)
{ {
this.LogVerbose("Client with ID {clientId} is not subscribed", clientId); this.LogVerbose("Client with ID {clientId} is not subscribed", clientId);
return; return;
} }
SubscriberIds.RemoveWhere((i) => i == clientId);
this.LogInformation("Client with ID {clientId} unsubscribed", clientId); this.LogInformation("Client with ID {clientId} unsubscribed", clientId);
} }
@@ -312,7 +326,14 @@ namespace PepperDash.Essentials.AppServer.Messengers
// If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties // If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties
if (string.IsNullOrEmpty(clientId)) if (string.IsNullOrEmpty(clientId))
{ {
foreach (var client in SubscriberIds) // Create a snapshot of subscribers to avoid collection modification during iteration
List<string> subscriberSnapshot;
lock (_subscriberLock)
{
subscriberSnapshot = new List<string>(subscriberIds);
}
foreach (var client in subscriberSnapshot)
{ {
AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content }); AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content });
} }

View File

@@ -37,10 +37,10 @@ namespace PepperDash.Essentials.Touchpanel
{ {
var theme = content.ToObject<MobileControlSimpleContent<string>>(); var theme = content.ToObject<MobileControlSimpleContent<string>>();
this.LogInformation("Setting theme to {theme}", this, theme.Value); this.LogInformation("Setting theme to {theme}", theme.Value);
_tpDevice.UpdateTheme(theme.Value); _tpDevice.UpdateTheme(theme.Value);
PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), id); PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), clientId: id);
}); });
} }
} }

View File

@@ -112,7 +112,7 @@ namespace PepperDash.Essentials.WebSocketServer
{ {
clientId = Id, clientId = Id,
roomKey = RoomKey, roomKey = RoomKey,
touchpanelKey = string.IsNullOrEmpty(TouchpanelKey) ? string.Empty : TouchpanelKey, touchpanelKey = TouchpanelKey ?? string.Empty,
}) })
}; };