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;
@@ -37,14 +36,14 @@ namespace PepperDash.Core
{ {
get get
{ {
return _DebugTimeoutInMs/60000; return _DebugTimeoutInMs / 60000;
} }
} }
/// <summary> /// <summary>
/// Gets or sets the RxStreamDebuggingIsEnabled /// Gets or sets the RxStreamDebuggingIsEnabled
/// </summary> /// </summary>
public bool RxStreamDebuggingIsEnabled{ get; private set; } public bool RxStreamDebuggingIsEnabled { get; private set; }
/// <summary> /// <summary>
/// Indicates that transmit stream debugging is enabled /// Indicates that transmit stream debugging is enabled
@@ -108,7 +107,7 @@ namespace PepperDash.Core
TxStreamDebuggingIsEnabled = true; TxStreamDebuggingIsEnabled = true;
Debug.SetDeviceDebugSettings(ParentDeviceKey, setting); Debug.SetDeviceDebugSettings(ParentDeviceKey, setting);
} }
/// <summary> /// <summary>
@@ -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)
_ClientStatus = value; {
OnConnectionChange(); if (_ClientStatus != value)
{
_ClientStatus = value;
shouldFireEvent = true;
}
}
// Fire event outside lock to avoid deadlock
if (shouldFireEvent)
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

@@ -19,44 +19,44 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; } public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary> /// <summary>
/// Fires when data is received from the server and returns it as a Byte array /// Fires when data is received from the server and returns it as a Byte array
/// </summary> /// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived; public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary> /// <summary>
/// Fires when data is received from the server and returns it as text /// Fires when data is received from the server and returns it as text
/// </summary> /// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived; public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange; //public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange; public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
private string _hostname; private string _hostname;
/// <summary> /// <summary>
/// Address of server /// Address of server
/// </summary> /// </summary>
public string Hostname public string Hostname
{ {
get get
{ {
return _hostname; return _hostname;
} }
set set
{ {
_hostname = value; _hostname = value;
if (_client != null) if (_client != null)
{ {
_client.AddressClientConnectedTo = _hostname; _client.AddressClientConnectedTo = _hostname;
} }
} }
} }
/// <summary> /// <summary>
/// Gets or sets the Port /// Gets or sets the Port
@@ -78,19 +78,19 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public int BufferSize { get; set; } public int BufferSize { get; set; }
/// <summary> /// <summary>
/// The actual client class /// The actual client class
/// </summary> /// </summary>
private TCPClient _client; private TCPClient _client;
/// <summary> /// <summary>
/// Bool showing if socket is connected /// Bool showing if socket is connected
/// </summary> /// </summary>
public bool IsConnected public bool IsConnected
{ {
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
} }
/// <summary> /// <summary>
/// S+ helper for IsConnected /// S+ helper for IsConnected
/// </summary> /// </summary>
@@ -99,15 +99,15 @@ namespace PepperDash.Core
get { return (ushort)(IsConnected ? 1 : 0); } get { return (ushort)(IsConnected ? 1 : 0); }
} }
/// <summary> /// <summary>
/// _client socket status Read only /// _client socket status Read only
/// </summary> /// </summary>
public SocketStatus ClientStatus public SocketStatus ClientStatus
{ {
get get
{ {
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus; return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
} }
} }
/// <summary> /// <summary>
@@ -119,26 +119,26 @@ namespace PepperDash.Core
get { return (ushort)ClientStatus; } get { return (ushort)ClientStatus; }
} }
/// <summary> /// <summary>
/// Status text shows the message associated with socket status /// Status text shows the message associated with socket status
/// </summary> /// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } } public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary> /// <summary>
/// Ushort representation of client status /// Ushort representation of client status
/// </summary> /// </summary>
[Obsolete] [Obsolete]
public ushort UClientStatus { get { return (ushort)ClientStatus; } } public ushort UClientStatus { get { return (ushort)ClientStatus; } }
/// <summary> /// <summary>
/// Connection failure reason /// Connection failure reason
/// </summary> /// </summary>
public string ConnectionFailure { get { return ClientStatus.ToString(); } } public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary> /// <summary>
/// Gets or sets the AutoReconnect /// Gets or sets the AutoReconnect
/// </summary> /// </summary>
public bool AutoReconnect { get; set; } public bool AutoReconnect { get; set; }
/// <summary> /// <summary>
/// S+ helper for AutoReconnect /// S+ helper for AutoReconnect
@@ -149,29 +149,29 @@ namespace PepperDash.Core
set { AutoReconnect = value == 1; } set { AutoReconnect = value == 1; }
} }
/// <summary> /// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000 /// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary> /// </summary>
public int AutoReconnectIntervalMs { get; set; } public int AutoReconnectIntervalMs { get; set; }
/// <summary> /// <summary>
/// Set only when the disconnect method is called /// Set only when the disconnect method is called
/// </summary> /// </summary>
bool DisconnectCalledByUser; bool DisconnectCalledByUser;
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public bool Connected public bool Connected
{ {
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; } get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
} }
//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 Timer for auto reconnect // private Timer for auto reconnect
private CTimer RetryTimer; private CTimer RetryTimer;
/// <summary> /// <summary>
/// Constructor /// Constructor
@@ -181,8 +181,8 @@ namespace PepperDash.Core
/// <param name="port"></param> /// <param name="port"></param>
/// <param name="bufferSize"></param> /// <param name="bufferSize"></param>
public GenericTcpIpClient(string key, string address, int port, int bufferSize) public GenericTcpIpClient(string key, string address, int port, int bufferSize)
: base(key) : base(key)
{ {
StreamDebugging = new CommunicationStreamDebugging(key); StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
@@ -218,18 +218,18 @@ namespace PepperDash.Core
/// Default constructor for S+ /// Default constructor for S+
/// </summary> /// </summary>
public GenericTcpIpClient() public GenericTcpIpClient()
: base(SplusKey) : base(SplusKey)
{ {
StreamDebugging = new CommunicationStreamDebugging(SplusKey); StreamDebugging = new CommunicationStreamDebugging(SplusKey);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
BufferSize = 2000; BufferSize = 2000;
RetryTimer = new CTimer(o => RetryTimer = new CTimer(o =>
{ {
Reconnect(); Reconnect();
}, Timeout.Infinite); }, Timeout.Infinite);
} }
/// <summary> /// <summary>
/// Initialize method /// Initialize method
@@ -255,26 +255,26 @@ namespace PepperDash.Core
/// ///
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
/// <summary> /// <summary>
/// Deactivate method /// Deactivate method
/// </summary> /// </summary>
public override bool Deactivate() public override bool Deactivate()
{ {
RetryTimer.Stop(); RetryTimer.Stop();
RetryTimer.Dispose(); RetryTimer.Dispose();
if (_client != null) if (_client != null)
{ {
_client.SocketStatusChange -= this.Client_SocketStatusChange; _client.SocketStatusChange -= this.Client_SocketStatusChange;
DisconnectClient(); DisconnectClient();
} }
return true; return true;
} }
/// <summary> /// <summary>
/// Connect method /// Connect method
/// </summary> /// </summary>
public void Connect() public void Connect()
{ {
if (string.IsNullOrEmpty(Hostname)) if (string.IsNullOrEmpty(Hostname))
{ {
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key); Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key);
@@ -310,7 +310,7 @@ namespace PepperDash.Core
{ {
connectLock.Leave(); connectLock.Leave();
} }
} }
private void Reconnect() private void Reconnect()
{ {
@@ -337,11 +337,11 @@ namespace PepperDash.Core
} }
} }
/// <summary> /// <summary>
/// Disconnect method /// Disconnect method
/// </summary> /// </summary>
public void Disconnect() public void Disconnect()
{ {
try try
{ {
connectLock.Enter(); connectLock.Enter();
@@ -355,7 +355,7 @@ namespace PepperDash.Core
{ {
connectLock.Leave(); connectLock.Leave();
} }
} }
/// <summary> /// <summary>
/// DisconnectClient method /// DisconnectClient method
@@ -375,7 +375,7 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="c"></param> /// <param name="c"></param>
void ConnectToServerCallback(TCPClient c) void ConnectToServerCallback(TCPClient c)
{ {
if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus); Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus);
@@ -385,13 +385,13 @@ namespace PepperDash.Core
{ {
Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus); Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus);
} }
} }
/// <summary> /// <summary>
/// Disconnects, waits and attemtps to connect again /// Disconnects, waits and attemtps to connect again
/// </summary> /// </summary>
void WaitAndTryReconnect() void WaitAndTryReconnect()
{ {
CrestronInvoke.BeginInvoke(o => CrestronInvoke.BeginInvoke(o =>
{ {
try try
@@ -409,7 +409,7 @@ namespace PepperDash.Core
connectLock.Leave(); connectLock.Leave();
} }
}); });
} }
/// <summary> /// <summary>
/// Recieves incoming data /// Recieves incoming data
@@ -417,7 +417,7 @@ namespace PepperDash.Core
/// <param name="client"></param> /// <param name="client"></param>
/// <param name="numBytes"></param> /// <param name="numBytes"></param>
void Receive(TCPClient client, int numBytes) void Receive(TCPClient client, int numBytes)
{ {
if (client != null) if (client != null)
{ {
if (numBytes > 0) if (numBytes > 0)
@@ -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,58 +434,53 @@ 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));
} }
} }
client.ReceiveDataAsync(Receive); client.ReceiveDataAsync(Receive);
} }
} }
/// <summary> /// <summary>
/// SendText method /// SendText method
/// </summary> /// </summary>
public void SendText(string text) public void SendText(string text)
{ {
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);
} }
/// <summary> /// <summary>
/// SendEscapedText method /// SendEscapedText method
/// </summary> /// </summary>
public void SendEscapedText(string text) public void SendEscapedText(string text)
{ {
var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s => var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s =>
{ {
var hex = s.Groups[1].Value; var hex = s.Groups[1].Value;
return ((char)Convert.ToByte(hex, 16)).ToString(); return ((char)Convert.ToByte(hex, 16)).ToString();
}); });
SendText(unescapedText); SendText(unescapedText);
} }
/// <summary> /// <summary>
/// Sends Bytes to the server /// Sends Bytes to the server
/// </summary> /// </summary>
/// <param name="bytes"></param> /// <param name="bytes"></param>
/// <summary> /// <summary>
/// SendBytes method /// SendBytes method
/// </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);
} }
/// <summary> /// <summary>
/// Socket Status Change Handler /// Socket Status Change Handler
@@ -496,7 +488,7 @@ namespace PepperDash.Core
/// <param name="client"></param> /// <param name="client"></param>
/// <param name="clientSocketStatus"></param> /// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus) void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
{ {
if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED) if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{ {
Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
@@ -505,68 +497,73 @@ namespace PepperDash.Core
else else
{ {
Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText); Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
_client.ReceiveDataAsync(Receive); _client.ReceiveDataAsync(Receive);
} }
var handler = ConnectionChange; var handler = ConnectionChange;
if (handler != null) if (handler != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this)); ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
} }
} }
/// <summary> /// <summary>
/// Represents a TcpSshPropertiesConfig /// Represents a TcpSshPropertiesConfig
/// </summary> /// </summary>
public class TcpSshPropertiesConfig public class TcpSshPropertiesConfig
{ {
/// <summary> /// <summary>
/// Address to connect to /// Address to connect to
/// </summary> /// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public string Address { get; set; } public string Address { get; set; }
/// <summary> /// <summary>
/// Port to connect to /// Port to connect to
/// </summary> /// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public int Port { get; set; } public int Port { get; set; }
/// <summary> /// <summary>
/// Username credential /// Username credential
/// </summary> /// </summary>
public string Username { get; set; } public string Username { get; set; }
/// <summary> /// <summary>
/// Gets or sets the Password /// Gets or sets the Password
/// </summary> /// </summary>
public string Password { get; set; } public string Password { get; set; }
/// <summary> /// <summary>
/// Defaults to 32768 /// Defaults to 32768
/// </summary> /// </summary>
public int BufferSize { get; set; } public int BufferSize { get; set; }
/// <summary> /// <summary>
/// Gets or sets the AutoReconnect /// Gets or sets the AutoReconnect
/// </summary> /// </summary>
public bool AutoReconnect { get; set; } public bool AutoReconnect { get; set; }
/// <summary> /// <summary>
/// Gets or sets the AutoReconnectIntervalMs /// Gets or sets the AutoReconnectIntervalMs
/// </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>
public TcpSshPropertiesConfig() public TcpSshPropertiesConfig()
{ {
BufferSize = 32768; BufferSize = 32768;
AutoReconnect = true; AutoReconnect = true;
AutoReconnectIntervalMs = 5000; AutoReconnectIntervalMs = 5000;
Username = ""; Username = "";
Password = ""; Password = "";
} DisableSshEcho = false;
}
} }
} }

View File

@@ -124,7 +124,7 @@ namespace PepperDash.Core
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
} }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -135,7 +135,7 @@ namespace PepperDash.Core
public GenericUdpServer(string key, string address, int port, int bufferSize) public GenericUdpServer(string key, string address, int port, int bufferSize)
: base(key) : base(key)
{ {
StreamDebugging = new CommunicationStreamDebugging(key); StreamDebugging = new CommunicationStreamDebugging(key);
Hostname = address; Hostname = address;
Port = port; Port = port;
BufferSize = bufferSize; BufferSize = bufferSize;
@@ -180,7 +180,7 @@ namespace PepperDash.Core
/// <param name="programEventType"></param> /// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{ {
if (programEventType != eProgramStatusEventType.Stopping) if (programEventType != eProgramStatusEventType.Stopping)
return; return;
Debug.Console(1, this, "Program stopping. Disabling Server"); Debug.Console(1, this, "Program stopping. Disabling Server");
@@ -243,7 +243,7 @@ namespace PepperDash.Core
/// </summary> /// </summary>
public void Disconnect() public void Disconnect()
{ {
if(Server != null) if (Server != null)
Server.DisableUDPServer(); Server.DisableUDPServer();
IsConnected = false; IsConnected = false;
@@ -265,7 +265,7 @@ namespace PepperDash.Core
try try
{ {
if (numBytes <= 0) if (numBytes <= 0)
return; return;
var sourceIp = Server.IPAddressLastMessageReceivedFrom; var sourceIp = Server.IPAddressLastMessageReceivedFrom;
@@ -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);
@@ -343,11 +337,11 @@ namespace PepperDash.Core
} }
/// <summary> /// <summary>
/// Represents a GenericUdpReceiveTextExtraArgs /// Represents a GenericUdpReceiveTextExtraArgs
/// </summary> /// </summary>
public class GenericUdpReceiveTextExtraArgs : EventArgs public class GenericUdpReceiveTextExtraArgs : EventArgs
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -359,7 +353,7 @@ namespace PepperDash.Core
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public int Port { get; private set; } public int Port { get; private set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -373,18 +367,18 @@ namespace PepperDash.Core
/// <param name="port"></param> /// <param name="port"></param>
/// <param name="bytes"></param> /// <param name="bytes"></param>
public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes) public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes)
{ {
Text = text; Text = text;
IpAddress = ipAddress; IpAddress = ipAddress;
Port = port; Port = port;
Bytes = bytes; Bytes = bytes;
} }
/// <summary> /// <summary>
/// Stupid S+ Constructor /// Stupid S+ Constructor
/// </summary> /// </summary>
public GenericUdpReceiveTextExtraArgs() { } public GenericUdpReceiveTextExtraArgs() { }
} }
/// <summary> /// <summary>
/// ///

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
@@ -42,7 +39,7 @@ namespace PepperDash.Core
/// Defines the contract for IBasicCommunication /// Defines the contract for IBasicCommunication
/// </summary> /// </summary>
public interface IBasicCommunication : ICommunicationReceiver public interface IBasicCommunication : ICommunicationReceiver
{ {
/// <summary> /// <summary>
/// Send text to the device /// Send text to the device
/// </summary> /// </summary>
@@ -54,7 +51,7 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="bytes"></param> /// <param name="bytes"></param>
void SendBytes(byte[] bytes); void SendBytes(byte[] bytes);
} }
/// <summary> /// <summary>
/// Represents a device that implements IBasicCommunication and IStreamDebugging /// Represents a device that implements IBasicCommunication and IStreamDebugging
@@ -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
@@ -76,12 +73,12 @@ namespace PepperDash.Core
CommunicationStreamDebugging StreamDebugging { get; } CommunicationStreamDebugging StreamDebugging { get; }
} }
/// <summary> /// <summary>
/// For IBasicCommunication classes that have SocketStatus. GenericSshClient, /// For IBasicCommunication classes that have SocketStatus. GenericSshClient,
/// GenericTcpIpClient /// GenericTcpIpClient
/// </summary> /// </summary>
public interface ISocketStatus : IBasicCommunication public interface ISocketStatus : IBasicCommunication
{ {
/// <summary> /// <summary>
/// Notifies of socket status changes /// Notifies of socket status changes
/// </summary> /// </summary>
@@ -93,7 +90,7 @@ namespace PepperDash.Core
[JsonProperty("clientStatus")] [JsonProperty("clientStatus")]
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
SocketStatus ClientStatus { get; } SocketStatus ClientStatus { get; }
} }
/// <summary> /// <summary>
/// Describes a device that implements ISocketStatus and IStreamDebugging /// Describes a device that implements ISocketStatus and IStreamDebugging
@@ -107,24 +104,24 @@ namespace PepperDash.Core
/// Describes a device that can automatically attempt to reconnect /// Describes a device that can automatically attempt to reconnect
/// </summary> /// </summary>
public interface IAutoReconnect public interface IAutoReconnect
{ {
/// <summary> /// <summary>
/// Enable automatic recconnect /// Enable automatic recconnect
/// </summary> /// </summary>
[JsonProperty("autoReconnect")] [JsonProperty("autoReconnect")]
bool AutoReconnect { get; set; } bool AutoReconnect { get; set; }
/// <summary> /// <summary>
/// Interval in ms to attempt automatic recconnections /// Interval in ms to attempt automatic recconnections
/// </summary> /// </summary>
[JsonProperty("autoReconnectIntervalMs")] [JsonProperty("autoReconnectIntervalMs")]
int AutoReconnectIntervalMs { get; set; } int AutoReconnectIntervalMs { get; set; }
} }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public enum eGenericCommMethodStatusChangeType public enum eGenericCommMethodStatusChangeType
{ {
/// <summary> /// <summary>
/// Connected /// Connected
/// </summary> /// </summary>
@@ -133,45 +130,45 @@ namespace PepperDash.Core
/// Disconnected /// Disconnected
/// </summary> /// </summary>
Disconnected Disconnected
} }
/// <summary> /// <summary>
/// This delegate defines handler for IBasicCommunication status changes /// This delegate defines handler for IBasicCommunication status changes
/// </summary> /// </summary>
/// <param name="comm">Device firing the status change</param> /// <param name="comm">Device firing the status change</param>
/// <param name="status"></param> /// <param name="status"></param>
public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status); public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status);
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public class GenericCommMethodReceiveBytesArgs : EventArgs public class GenericCommMethodReceiveBytesArgs : EventArgs
{ {
/// <summary> /// <summary>
/// Gets or sets the Bytes /// Gets or sets the Bytes
/// </summary> /// </summary>
public byte[] Bytes { get; private set; } public byte[] Bytes { get; private set; }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
/// <param name="bytes"></param> /// <param name="bytes"></param>
public GenericCommMethodReceiveBytesArgs(byte[] bytes) public GenericCommMethodReceiveBytesArgs(byte[] bytes)
{ {
Bytes = bytes; Bytes = bytes;
} }
/// <summary> /// <summary>
/// S+ Constructor /// S+ Constructor
/// </summary> /// </summary>
public GenericCommMethodReceiveBytesArgs() { } public GenericCommMethodReceiveBytesArgs() { }
} }
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
public class GenericCommMethodReceiveTextArgs : EventArgs public class GenericCommMethodReceiveTextArgs : EventArgs
{ {
/// <summary> /// <summary>
/// ///
/// </summary> /// </summary>
@@ -185,9 +182,9 @@ namespace PepperDash.Core
/// </summary> /// </summary>
/// <param name="text"></param> /// <param name="text"></param>
public GenericCommMethodReceiveTextArgs(string text) public GenericCommMethodReceiveTextArgs(string text)
{ {
Text = text; Text = text;
} }
/// <summary> /// <summary>
/// ///
@@ -195,59 +192,14 @@ namespace PepperDash.Core
/// <param name="text"></param> /// <param name="text"></param>
/// <param name="delimiter"></param> /// <param name="delimiter"></param>
public GenericCommMethodReceiveTextArgs(string text, string delimiter) public GenericCommMethodReceiveTextArgs(string text, string delimiter)
:this(text) : this(text)
{ {
Delimiter = delimiter; Delimiter = delimiter;
} }
/// <summary>
/// S+ Constructor
/// </summary>
public GenericCommMethodReceiveTextArgs() { }
}
/// <summary>
///
/// </summary>
public class ComTextHelper
{
/// <summary> /// <summary>
/// Gets escaped text for a byte array /// S+ Constructor
/// </summary> /// </summary>
/// <param name="bytes"></param> public GenericCommMethodReceiveTextArgs() { }
/// <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

@@ -12,15 +12,15 @@ using Serilog.Events;
namespace PepperDash.Essentials.Core namespace PepperDash.Essentials.Core
{ {
/// <summary> /// <summary>
/// Represents a CecPortController /// Represents a CecPortController
/// </summary> /// </summary>
public class CecPortController : Device, IBasicCommunicationWithStreamDebugging public class CecPortController : Device, IBasicCommunicationWithStreamDebugging
{ {
/// <summary> /// <summary>
/// Gets or sets the StreamDebugging /// Gets or sets the StreamDebugging
/// </summary> /// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; } public CommunicationStreamDebugging StreamDebugging { get; private set; }
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived; public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived; public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
@@ -33,16 +33,16 @@ namespace PepperDash.Essentials.Core
ICec Port; ICec Port;
public CecPortController(string key, Func<EssentialsControlPropertiesConfig, ICec> postActivationFunc, public CecPortController(string key, Func<EssentialsControlPropertiesConfig, ICec> postActivationFunc,
EssentialsControlPropertiesConfig config):base(key) EssentialsControlPropertiesConfig config) : base(key)
{ {
StreamDebugging = new CommunicationStreamDebugging(key); StreamDebugging = new CommunicationStreamDebugging(key);
AddPostActivationAction(() => AddPostActivationAction(() =>
{ {
Port = postActivationFunc(config); Port = postActivationFunc(config);
Port.StreamCec.CecChange += StreamCec_CecChange; Port.StreamCec.CecChange += StreamCec_CecChange;
}); });
} }
public CecPortController(string key, ICec port) public CecPortController(string key, ICec port)
@@ -58,27 +58,25 @@ namespace PepperDash.Essentials.Core
if (args.EventId == CecEventIds.CecMessageReceivedEventId) if (args.EventId == CecEventIds.CecMessageReceivedEventId)
OnDataReceived(cecDevice.Received.StringValue); OnDataReceived(cecDevice.Received.StringValue);
else if (args.EventId == CecEventIds.ErrorFeedbackEventId) else if (args.EventId == CecEventIds.ErrorFeedbackEventId)
if(cecDevice.ErrorFeedback.BoolValue) if (cecDevice.ErrorFeedback.BoolValue)
Debug.LogMessage(LogEventLevel.Verbose, this, "CEC NAK Error"); Debug.LogMessage(LogEventLevel.Verbose, this, "CEC NAK Error");
} }
void OnDataReceived(string s) void OnDataReceived(string s)
{ {
var bytesHandler = BytesReceived; var bytesHandler = BytesReceived;
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)); }
}
} }
#region IBasicCommunication Members #region IBasicCommunication Members
@@ -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,8 +100,8 @@ 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;
@@ -23,10 +25,10 @@ namespace PepperDash.Essentials.Core
/// Event fired when bytes are received /// Event fired when bytes are received
/// </summary> /// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived; public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary> /// <summary>
/// Event fired when text is received /// Event fired when text is received
/// </summary> /// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived; public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary> /// <summary>
@@ -38,12 +40,12 @@ namespace PepperDash.Essentials.Core
ComPort.ComPortSpec Spec; ComPort.ComPortSpec Spec;
/// <summary> /// <summary>
/// Constructor /// Constructor
/// </summary> /// </summary>
/// <param name="key"></param> /// <param name="key"></param>
/// <param name="postActivationFunc"></param> /// <param name="postActivationFunc"></param>
/// <param name="spec"></param> /// <param name="spec"></param>
/// <param name="config"></param> /// <param name="config"></param>
public ComPortController(string key, Func<EssentialsControlPropertiesConfig, ComPort> postActivationFunc, public ComPortController(string key, Func<EssentialsControlPropertiesConfig, ComPort> postActivationFunc,
ComPort.ComPortSpec spec, EssentialsControlPropertiesConfig config) : base(key) ComPort.ComPortSpec spec, EssentialsControlPropertiesConfig config) : base(key)
{ {
@@ -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

@@ -6,9 +6,9 @@ using PepperDash.Core.Web.RequestHandlers;
namespace PepperDash.Essentials.Core.Web.RequestHandlers namespace PepperDash.Essentials.Core.Web.RequestHandlers
{ {
/// <summary> /// <summary>
/// Represents a SetDeviceStreamDebugRequestHandler /// Represents a SetDeviceStreamDebugRequestHandler
/// </summary> /// </summary>
public class SetDeviceStreamDebugRequestHandler : WebApiBaseRequestHandler public class SetDeviceStreamDebugRequestHandler : WebApiBaseRequestHandler
{ {
/// <summary> /// <summary>
@@ -122,23 +122,23 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
return; return;
} }
if (!(DeviceManager.GetDeviceForKey(body.DeviceKey) is IStreamDebugging device)) if (!(DeviceManager.GetDeviceForKey(body.DeviceKey) is IStreamDebugging device))
{ {
context.Response.StatusCode = 404; context.Response.StatusCode = 404;
context.Response.StatusDescription = "Not Found"; context.Response.StatusDescription = "Not Found";
context.Response.End(); context.Response.End();
return; return;
} }
eStreamDebuggingSetting debugSetting; eStreamDebuggingSetting debugSetting;
try try
{ {
debugSetting = (eStreamDebuggingSetting) Enum.Parse(typeof (eStreamDebuggingSetting), body.Setting, true); debugSetting = (eStreamDebuggingSetting)Enum.Parse(typeof(eStreamDebuggingSetting), body.Setting, true);
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogMessage(ex, "Exception handling set debug request"); Debug.LogMessage(ex, "Exception handling set debug request");
context.Response.StatusCode = 500; context.Response.StatusCode = 500;
context.Response.StatusDescription = "Internal Server Error"; context.Response.StatusDescription = "Internal Server Error";
context.Response.End(); context.Response.End();
@@ -164,7 +164,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
} }
catch (Exception ex) catch (Exception ex)
{ {
Debug.LogMessage(ex, "Exception handling set debug request"); Debug.LogMessage(ex, "Exception handling set debug request");
context.Response.StatusCode = 500; context.Response.StatusCode = 500;
context.Response.StatusDescription = "Internal Server Error"; context.Response.StatusDescription = "Internal Server Error";
context.Response.End(); context.Response.End();
@@ -198,21 +198,21 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
public class SetDeviceStreamDebugConfig public class SetDeviceStreamDebugConfig
{ {
[JsonProperty("deviceKey", NullValueHandling = NullValueHandling.Include)] [JsonProperty("deviceKey", NullValueHandling = NullValueHandling.Include)]
/// <summary> /// <summary>
/// Gets or sets the DeviceKey /// Gets or sets the DeviceKey
/// </summary> /// </summary>
public string DeviceKey { get; set; } public string DeviceKey { get; set; }
[JsonProperty("setting", NullValueHandling = NullValueHandling.Include)] [JsonProperty("setting", NullValueHandling = NullValueHandling.Include)]
/// <summary> /// <summary>
/// Gets or sets the Setting /// Gets or sets the Setting
/// </summary> /// </summary>
public string Setting { get; set; } public string Setting { get; set; }
[JsonProperty("timeout")] [JsonProperty("timeout")]
/// <summary> /// <summary>
/// Gets or sets the Timeout /// Gets or sets the Timeout
/// </summary> /// </summary>
public int Timeout { get; set; } public int Timeout { get; set; }
public SetDeviceStreamDebugConfig() public SetDeviceStreamDebugConfig()

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,14 +198,15 @@ namespace PepperDash.Essentials.AppServer.Messengers
return; return;
} }
if (SubscriberIds.Any(id => id == clientId)) lock (_subscriberLock)
{ {
this.LogVerbose("Client {clientId} already subscribed", clientId); if (!subscriberIds.Add(clientId))
return; {
this.LogVerbose("Client {clientId} already subscribed", clientId);
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,
}) })
}; };