Compare commits

...

23 Commits

Author SHA1 Message Date
jkdevito
9ff139bc00 fix: update TcpIpServer case to TcpServer in CreateCommForDevice method 2025-11-19 17:31:00 -06:00
jkdevito
563de96e7c feat: add TcpIpServer case to CreateCommForDevice method 2025-11-19 17:28:54 -06:00
jkdevito
dd5d1457d2 feat: add TcpServer control method to eControlMethod enum 2025-11-19 17:28:46 -06:00
Neil Dorin
2fbc32947c Merge pull request #1357 from PepperDash/url-parsing
fix: check for multiple URL patterns for both template & system URLS
2025-11-18 14:14:16 -05:00
Andrew Welker
c06b57a5f9 fix: check for multiple URL patterns for both template & system URLS 2025-11-18 12:30:54 -06:00
Neil Dorin
6d64fffc50 Merge pull request #1356 from PepperDash/mc-subscription-logging
Multiple Fixes
2025-11-14 15:44:58 -05:00
Andrew Welker
0c4ebdaf1d fix: change how subscription state is logged to reduce traffic to console 2025-11-13 09:56:29 -06:00
Andrew Welker
2c49fb9321 fix: parse current Portal URLS for system and template UUIDs correctly 2025-11-12 16:58:23 -06:00
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
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
15 changed files with 327 additions and 191 deletions

View File

@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
namespace PepperDash.Core
{
/// <summary>
///
/// Helper class for formatting communication text and byte data for debugging purposes.
/// </summary>
public class ComTextHelper
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Crestron.SimplSharp;
@@ -11,11 +12,12 @@ using Renci.SshNet.Common;
namespace PepperDash.Core
{
/// <summary>
///
/// SSH Client
/// </summary>
public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SPlusKey = "Uninitialized SshClient";
/// <summary>
/// Object to enable stream debugging
/// </summary>
@@ -36,11 +38,6 @@ namespace PepperDash.Core
/// </summary>
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
/////
///// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
/// <summary>
/// Gets or sets the Hostname
/// </summary>
@@ -67,7 +64,7 @@ namespace PepperDash.Core
public bool IsConnected
{
// 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>
@@ -83,16 +80,26 @@ namespace PepperDash.Core
/// </summary>
public SocketStatus ClientStatus
{
get { return _ClientStatus; }
get { lock (_stateLock) { return _ClientStatus; } }
private set
{
if (_ClientStatus == value)
return;
_ClientStatus = value;
OnConnectionChange();
bool shouldFireEvent = false;
lock (_stateLock)
{
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>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
@@ -100,7 +107,7 @@ namespace PepperDash.Core
/// </summary>
public ushort UStatus
{
get { return (ushort)_ClientStatus; }
get { lock (_stateLock) { return (ushort)_ClientStatus; } }
}
/// <summary>
@@ -111,7 +118,11 @@ namespace PepperDash.Core
/// <summary>
/// Will be set and unset by connect and disconnect only
/// </summary>
public bool ConnectEnabled { get; private set; }
public bool ConnectEnabled
{
get { lock (_stateLock) { return _ConnectEnabled; } }
private set { lock (_stateLock) { _ConnectEnabled = value; } }
}
/// <summary>
/// S+ helper for AutoReconnect
@@ -127,17 +138,25 @@ namespace PepperDash.Core
/// </summary>
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
//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>
/// Typical constructor.
@@ -154,13 +173,13 @@ namespace PepperDash.Core
Password = password;
AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o =>
reconnectTimer = new Timer(o =>
{
if (ConnectEnabled)
if (ConnectEnabled) // Now thread-safe property access
{
Connect();
}
}, System.Threading.Timeout.Infinite);
}, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
}
/// <summary>
@@ -172,23 +191,23 @@ namespace PepperDash.Core
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o =>
reconnectTimer = new Timer(o =>
{
if (ConnectEnabled)
if (ConnectEnabled) // Now thread-safe property access
{
Connect();
}
}, System.Threading.Timeout.Infinite);
}, null, System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite);
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
if (Client != null)
if (client != null)
{
this.LogDebug("Program stopping. Closing connection");
Disconnect();
@@ -223,10 +242,10 @@ namespace PepperDash.Core
this.LogDebug("Attempting connect");
// Cancel reconnect if running.
ReconnectTimer?.Stop();
StopReconnectTimer();
// Cleanup the old client if it already exists
if (Client != null)
if (client != null)
{
this.LogDebug("Cleaning up disconnected client");
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
@@ -239,29 +258,36 @@ namespace PepperDash.Core
this.LogDebug("Creating new SshClient");
ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth);
Client = new SshClient(connectionInfo);
Client.ErrorOccurred += Client_ErrorOccurred;
client = new SshClient(connectionInfo);
client.ErrorOccurred += Client_ErrorOccurred;
//Attempt to connect
ClientStatus = SocketStatus.SOCKET_STATUS_WAITING;
try
{
Client.Connect();
TheStream = Client.CreateShellStream("PDTShell", 0, 0, 0, 0, 65534);
if (TheStream.DataAvailable)
client.Connect();
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
string str = TheStream.Read();
shellStream.Read();
}
TheStream.DataReceived += Stream_DataReceived;
shellStream.DataReceived += Stream_DataReceived;
this.LogInformation("Connected");
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
DisconnectLogged = false;
disconnectLogged = false;
}
catch (SshConnectionException e)
{
var ie = e.InnerException; // The details are inside!!
var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error;
if (ie is SocketException)
{
@@ -286,37 +312,36 @@ namespace PepperDash.Core
this.LogVerbose(ie, "Exception details: ");
}
DisconnectLogged = true;
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
StartReconnectTimer();
}
}
catch (SshOperationTimeoutException ex)
{
this.LogWarning("Connection attempt timed out: {message}", ex.Message);
DisconnectLogged = true;
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
StartReconnectTimer();
}
}
catch (Exception e)
{
var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error;
this.LogError("Unhandled exception on connect: {error}", e.Message);
this.LogVerbose(e, "Exception details: ");
DisconnectLogged = true;
disconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
StartReconnectTimer();
}
}
}
@@ -334,11 +359,7 @@ namespace PepperDash.Core
{
ConnectEnabled = false;
// Stop trying reconnects, if we are
if (ReconnectTimer != null)
{
ReconnectTimer.Stop();
// ReconnectTimer = null;
}
StopReconnectTimer();
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
}
@@ -352,12 +373,12 @@ namespace PepperDash.Core
try
{
if (Client != null)
if (client != null)
{
Client.ErrorOccurred -= Client_ErrorOccurred;
Client.Disconnect();
Client.Dispose();
Client = null;
client.ErrorOccurred -= Client_ErrorOccurred;
client.Disconnect();
client.Dispose();
client = null;
ClientStatus = status;
this.LogDebug("Disconnected");
}
@@ -371,16 +392,16 @@ namespace PepperDash.Core
/// <summary>
/// Kills the stream
/// </summary>
void KillStream()
private void KillStream()
{
try
{
if (TheStream != null)
if (shellStream != null)
{
TheStream.DataReceived -= Stream_DataReceived;
TheStream.Close();
TheStream.Dispose();
TheStream = null;
shellStream.DataReceived -= Stream_DataReceived;
shellStream.Close();
shellStream.Dispose();
shellStream = null;
this.LogDebug("Disconnected stream");
}
}
@@ -393,7 +414,7 @@ namespace PepperDash.Core
/// <summary>
/// Handles the keyboard interactive authentication, should it be required.
/// </summary>
void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
private void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
{
foreach (AuthenticationPrompt prompt in e.Prompts)
if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
@@ -403,7 +424,7 @@ namespace PepperDash.Core
/// <summary>
/// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
/// </summary>
void Stream_DataReceived(object sender, ShellDataEventArgs e)
private void Stream_DataReceived(object sender, ShellDataEventArgs e)
{
if (((ShellStream)sender).Length <= 0L)
{
@@ -435,7 +456,7 @@ namespace PepperDash.Core
/// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
/// event
/// </summary>
void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
private void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
{
CrestronInvoke.BeginInvoke(o =>
{
@@ -455,7 +476,7 @@ namespace PepperDash.Core
if (AutoReconnect && ConnectEnabled)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
StartReconnectTimer();
}
});
}
@@ -463,7 +484,7 @@ namespace PepperDash.Core
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
private void OnConnectionChange()
{
ConnectionChange?.Invoke(this, new GenericSocketStatusChageEventArgs(this));
}
@@ -478,12 +499,12 @@ namespace PepperDash.Core
{
try
{
if (Client != null && TheStream != null && IsConnected)
if (client != null && shellStream != null && IsConnected)
{
this.PrintSentText(text);
TheStream.Write(text);
TheStream.Flush();
shellStream.Write(text);
shellStream.Flush();
}
else
{
@@ -495,7 +516,7 @@ namespace PepperDash.Core
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
ReconnectTimer.Reset();
StartReconnectTimer();
}
catch (Exception ex)
{
@@ -511,12 +532,12 @@ namespace PepperDash.Core
{
try
{
if (Client != null && TheStream != null && IsConnected)
if (client != null && shellStream != null && IsConnected)
{
this.PrintSentBytes(bytes);
TheStream.Write(bytes, 0, bytes.Length);
TheStream.Flush();
shellStream.Write(bytes, 0, bytes.Length);
shellStream.Flush();
}
else
{
@@ -528,7 +549,7 @@ namespace PepperDash.Core
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
ReconnectTimer.Reset();
StartReconnectTimer();
}
catch (Exception ex)
{
@@ -537,6 +558,83 @@ namespace PepperDash.Core
}
#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

@@ -477,8 +477,7 @@ namespace PepperDash.Core
/// </summary>
public void SendBytes(byte[] bytes)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
this.PrintSentBytes(bytes);
if (_client != null)
_client.SendData(bytes, bytes.Length);
}
@@ -548,6 +547,12 @@ namespace PepperDash.Core
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// When true, turns off echo for the SSH session
/// </summary>
[JsonProperty("disableSshEcho")]
public bool DisableSshEcho { get; set; }
/// <summary>
/// Default constructor
/// </summary>
@@ -558,8 +563,7 @@ namespace PepperDash.Core
AutoReconnectIntervalMs = 5000;
Username = "";
Password = "";
DisableSshEcho = false;
}
}
}

View File

@@ -82,6 +82,10 @@ namespace PepperDash.Core
/// <summary>
/// InfinetEX control
/// </summary>
InfinetEx
InfinetEx,
/// <summary>
/// TCP/IP Server
/// </summary>
TcpServer
}
}

View File

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

View File

@@ -168,7 +168,7 @@ namespace PepperDash.Core
.WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath,
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: LogEventLevel.Debug,
retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60,
retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 7 : 14,
levelSwitch: _fileLogLevelSwitch
);
@@ -1081,9 +1081,6 @@ namespace PepperDash.Core
/// Logs to Console when at-level, and all messages to error log
/// </summary>
[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,
string format, params object[] items)
{
@@ -1096,9 +1093,6 @@ namespace PepperDash.Core
/// it will only be written to the log.
/// </summary>
[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)
{
LogMessage(level, format, items);

View File

@@ -1,7 +1,9 @@
using System;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharpPro;
using Crestron.SimplSharpPro.GeneralIO;
using PepperDash.Core;
using PepperDash.Core.Logging;
using Serilog.Events;
@@ -85,61 +87,29 @@ namespace PepperDash.Essentials.Core
{
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;
}
// 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 = genericDevice.Register();
var result = Port.Register();
if (result != eDeviceRegistrationUnRegistrationResponse.Success)
{
this.LogError($"ERROR: Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})");
return; // false
this.LogError($"Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})");
return;
}
this.LogInformation($"Successfully registered {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})");
}
var specResult = Port.SetComPortSpec(Spec);
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;
}
//this.LogInformation($"INFO: 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}'");
// };
// }
this.LogInformation($"Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})");
Port.SerialDataReceived += Port_SerialDataReceived;
}

View File

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

View File

@@ -11,11 +11,11 @@ using PepperDash.Core;
namespace PepperDash.Essentials.Core.Config
{
/// <summary>
/// Loads the ConfigObject from the file
/// </summary>
public class EssentialsConfig : BasicConfig
{
/// <summary>
/// Loads the ConfigObject from the file
/// </summary>
public class EssentialsConfig : BasicConfig
{
/// <summary>
/// Gets or sets the SystemUrl
/// </summary>
@@ -32,24 +32,33 @@ namespace PepperDash.Essentials.Core.Config
/// Gets the SystemUuid extracted from the SystemUrl
/// </summary>
[JsonProperty("systemUuid")]
public string SystemUuid
public string SystemUuid
{
get
{
if (string.IsNullOrEmpty(SystemUrl))
return "missing url";
string uuid;
if (SystemUrl.Contains("#"))
{
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*");
string uuid = result.Groups[1].Value;
return uuid;
} else
{
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/.*");
string uuid = result.Groups[1].Value;
return uuid;
if (string.IsNullOrEmpty(SystemUrl))
{
uuid = "missing url";
}
else if (SystemUrl.Contains("#"))
{
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*");
uuid = result.Groups[1].Value;
}
else if (SystemUrl.Contains("detail"))
{
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/detail\/(.*)\/.*");
uuid = result.Groups[1].Value;
}
else
{
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/.*");
uuid = result.Groups[1].Value;
}
return uuid;
}
}
@@ -57,24 +66,33 @@ namespace PepperDash.Essentials.Core.Config
/// Gets the TemplateUuid extracted from the TemplateUrl
/// </summary>
[JsonProperty("templateUuid")]
public string TemplateUuid
public string TemplateUuid
{
get
{
if (string.IsNullOrEmpty(TemplateUrl))
return "missing template url";
string uuid;
if (TemplateUrl.Contains("#"))
{
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*");
string uuid = result.Groups[1].Value;
return uuid;
} else
{
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*");
string uuid = result.Groups[2].Value;
return uuid;
if (string.IsNullOrEmpty(TemplateUrl))
{
uuid = "missing template url";
}
else if (TemplateUrl.Contains("#"))
{
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*");
uuid = result.Groups[1].Value;
}
else if (TemplateUrl.Contains("detail"))
{
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/detail\/(.*)\/system-template-versions\/detail\/(.*)\/.*");
uuid = result.Groups[2].Value;
}
else
{
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*");
uuid = result.Groups[2].Value;
}
return uuid;
}
}
@@ -97,7 +115,7 @@ namespace PepperDash.Essentials.Core.Config
{
Rooms = new List<DeviceConfig>();
}
}
}
/// <summary>
/// Represents version data for Essentials and its packages
@@ -147,7 +165,7 @@ namespace PepperDash.Essentials.Core.Config
/// Represents a SystemTemplateConfigs
/// </summary>
public class SystemTemplateConfigs
{
{
/// <summary>
/// Gets or sets the System
/// </summary>
@@ -157,5 +175,5 @@ namespace PepperDash.Essentials.Core.Config
/// Gets or sets the Template
/// </summary>
public EssentialsConfig Template { get; set; }
}
}
}

View File

@@ -98,7 +98,7 @@ namespace PepperDash.Essentials.Core.Routing
/// <param name="inputPort">The currently selected input port on the destination device.</param>
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)
{

View File

@@ -9,7 +9,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic
/// <summary>
/// Represents a GenericSink
/// </summary>
public class GenericSink : EssentialsDevice, IRoutingSinkWithInputPort
public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort
{
/// <summary>
/// Initializes a new instance of the GenericSink class
@@ -20,7 +20,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic
{
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);
}
@@ -66,6 +66,15 @@ namespace PepperDash.Essentials.Devices.Common.Generic
/// Event fired when the current source changes
/// </summary>
public event SourceInfoChangeHandler CurrentSourceChange;
/// <inheritdoc />
public event InputChangedEventHandler InputChanged;
/// <inheritdoc />
public void ExecuteSwitch(object inputSelector)
{
throw new System.NotImplementedException();
}
}
/// <summary>

View File

@@ -31,7 +31,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
/// <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.
/// </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;
@@ -189,18 +194,18 @@ namespace PepperDash.Essentials.AppServer.Messengers
{
if (!enableMessengerSubscriptions)
{
this.LogWarning("Messenger subscriptions not enabled");
return;
}
if (SubscriberIds.Any(id => id == clientId))
lock (_subscriberLock)
{
this.LogVerbose("Client {clientId} already subscribed", clientId);
return;
if (!subscriberIds.Add(clientId))
{
this.LogVerbose("Client {clientId} already subscribed", clientId);
return;
}
}
SubscriberIds.Add(clientId);
this.LogDebug("Client {clientId} subscribed", clientId);
}
@@ -212,19 +217,26 @@ namespace PepperDash.Essentials.AppServer.Messengers
{
if (!enableMessengerSubscriptions)
{
this.LogWarning("Messenger subscriptions not enabled");
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);
return;
}
SubscriberIds.RemoveWhere((i) => i == clientId);
this.LogInformation("Client with ID {clientId} unsubscribed", clientId);
this.LogDebug("Client with ID {clientId} unsubscribed", clientId);
}
/// <summary>
@@ -258,7 +270,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
}
catch (Exception ex)
{
this.LogError(ex, "Exception posting status message for {messagePath} to {clientId}", MessagePath, clientId ?? "all clients");
this.LogError("Exception posting status message for {messagePath} to {clientId}: {message}", MessagePath, clientId ?? "all clients", ex.Message);
this.LogDebug(ex, "Stack trace: ");
}
}
@@ -287,7 +300,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
}
catch (Exception ex)
{
this.LogError(ex, "Exception posting status message for {type} to {clientId}", type, clientId ?? "all clients");
this.LogError("Exception posting status message for {type} to {clientId}: {message}", type, clientId ?? "all clients", ex.Message);
this.LogDebug(ex, "Stack trace: ");
}
}
@@ -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 (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 });
}

View File

@@ -1312,6 +1312,11 @@ namespace PepperDash.Essentials
/// <inheritdoc />
public override void Initialize()
{
if (!Config.EnableMessengerSubscriptions)
{
this.LogWarning("Messenger subscriptions disabled. add \"enableMessengerSubscriptions\": true to config for {key} to enable.", Key);
}
foreach (var messenger in _messengers)
{
try

View File

@@ -40,7 +40,7 @@ namespace PepperDash.Essentials.Touchpanel
this.LogInformation("Setting theme to {theme}", theme.Value);
_tpDevice.UpdateTheme(theme.Value);
PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), id);
PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), clientId: id);
});
}
}