feat: move PD Core into Essentials

This commit is contained in:
Andrew Welker
2025-03-25 23:22:18 -05:00
parent cdafaf7586
commit 26116d0495
84 changed files with 16472 additions and 5 deletions

View File

@@ -4,19 +4,38 @@ Microsoft Visual Studio Solution File, Format Version 12.00
VisualStudioVersion = 17.4.33213.308
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Devices.Common", "src\PepperDash.Essentials.Devices.Common\PepperDash.Essentials.Devices.Common.csproj", "{53E204B7-97DD-441D-A96C-721DF014DF82}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials", "src\PepperDash.Essentials\PepperDash.Essentials.csproj", "{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Core", "src\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj", "{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mobile Control", "Mobile Control", "{B24989D7-32B5-48D5-9AE1-5F3B17D25206}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl", "src\PepperDash.Essentials.MobileControl\PepperDash.Essentials.MobileControl.csproj", "{F6D362DE-2256-44B1-927A-8CE4705D839A}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl.Messengers", "src\PepperDash.Essentials.MobileControl.Messengers\PepperDash.Essentials.MobileControl.Messengers.csproj", "{B438694F-8FF7-464A-9EC8-10427374471F}"
ProjectSection(ProjectDependencies) = postProject
{E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90}
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Essentials", "Essentials", "{AD98B742-8D85-481C-A69D-D8D8ABED39EA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU
@@ -54,6 +73,12 @@ Global
{B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.Build.0 = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -64,6 +89,7 @@ Global
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
{F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
{B438694F-8FF7-464A-9EC8-10427374471F} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206}
{E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3}

View File

@@ -33,6 +33,11 @@
</Delete>
<Message Text="Deleted files: '@(DeletedList)'" />
</Target>
<Target Name="Copy CLZ" AfterTargets="SimplSharpPostProcess" Condition="($(ProjectType) == 'Library')">
<Message Text="Copying CLZ"></Message>
<Move SourceFiles="$(TargetDir)\$(TargetName).clz" DestinationFiles="$(TargetDir)\$(TargetName).$(Version).$(TargetFramework).clz"/>
<Copy SourceFiles="$(TargetDir)\$(TargetName).$(Version).$(TargetFramework).clz" DestinationFiles="$(PackageOutputPath)\$(TargetName).$(Version).$(TargetFramework).clz"/>
</Target>
<Target Name="Copy CPZ" AfterTargets="SimplSharpPostProcess" Condition="($(ProjectType) == 'Program' And ( '$(TargetFramework)' != 'net6.0' ) And ( '$(TargetFramework)' != 'net8.0' ))">
<Message Text="Copying CPZ"></Message>
<Move SourceFiles="$(TargetDir)$(TargetName).cpz" DestinationFiles="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" />

View File

@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using PepperDash.Core;
namespace PepperDash.Core
{
/// <summary>
/// Defines the string event handler for line events on the gather
/// </summary>
/// <param name="text"></param>
public delegate void LineReceivedHandler(string text);
/// <summary>
/// Attaches to IBasicCommunication as a text gather
/// </summary>
public class CommunicationGather
{
/// <summary>
/// Event that fires when a line is received from the IBasicCommunication source.
/// The event merely contains the text, not an EventArgs type class.
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> LineReceived;
/// <summary>
/// The communication port that this gathers on
/// </summary>
public ICommunicationReceiver Port { get; private set; }
/// <summary>
/// Default false. If true, the delimiter will be included in the line output
/// events
/// </summary>
public bool IncludeDelimiter { get; set; }
/// <summary>
/// For receive buffer
/// </summary>
StringBuilder ReceiveBuffer = new StringBuilder();
/// <summary>
/// Delimiter, like it says!
/// </summary>
char Delimiter;
string[] StringDelimiters;
/// <summary>
/// Constructor for using a char delimiter
/// </summary>
/// <param name="port"></param>
/// <param name="delimiter"></param>
public CommunicationGather(ICommunicationReceiver port, char delimiter)
{
Port = port;
Delimiter = delimiter;
port.TextReceived += new EventHandler<GenericCommMethodReceiveTextArgs>(Port_TextReceived);
}
/// <summary>
/// Constructor for using a single string delimiter
/// </summary>
/// <param name="port"></param>
/// <param name="delimiter"></param>
public CommunicationGather(ICommunicationReceiver port, string delimiter)
:this(port, new string[] { delimiter} )
{
}
/// <summary>
/// Constructor for using an array of string delimiters
/// </summary>
/// <param name="port"></param>
/// <param name="delimiters"></param>
public CommunicationGather(ICommunicationReceiver port, string[] delimiters)
{
Port = port;
StringDelimiters = delimiters;
port.TextReceived += Port_TextReceivedStringDelimiter;
}
/// <summary>
/// Disconnects this gather from the Port's TextReceived event. This will not fire LineReceived
/// after the this call.
/// </summary>
public void Stop()
{
Port.TextReceived -= Port_TextReceived;
Port.TextReceived -= Port_TextReceivedStringDelimiter;
}
/// <summary>
/// Handler for raw data coming from port
/// </summary>
void Port_TextReceived(object sender, GenericCommMethodReceiveTextArgs args)
{
var handler = LineReceived;
if (handler != null)
{
ReceiveBuffer.Append(args.Text);
var str = ReceiveBuffer.ToString();
var lines = str.Split(Delimiter);
if (lines.Length > 0)
{
for (int i = 0; i < lines.Length - 1; i++)
{
string strToSend = null;
if (IncludeDelimiter)
strToSend = lines[i] + Delimiter;
else
strToSend = lines[i];
handler(this, new GenericCommMethodReceiveTextArgs(strToSend));
}
ReceiveBuffer = new StringBuilder(lines[lines.Length - 1]);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
void Port_TextReceivedStringDelimiter(object sender, GenericCommMethodReceiveTextArgs args)
{
var handler = LineReceived;
if (handler != null)
{
// Receive buffer should either be empty or not contain the delimiter
// If the line does not have a delimiter, append the
ReceiveBuffer.Append(args.Text);
var str = ReceiveBuffer.ToString();
// Case: Receiving DEVICE get version\x0d\0x0a+OK "value":"1234"\x0d\x0a
// RX: DEV
// Split: (1) "DEV"
// RX: I
// Split: (1) "DEVI"
// RX: CE get version
// Split: (1) "DEVICE get version"
// RX: \x0d\x0a+OK "value":"1234"\x0d\x0a
// Split: (2) DEVICE get version, +OK "value":"1234"
// Iterate the delimiters and fire an event for any matching delimiter
foreach (var delimiter in StringDelimiters)
{
var lines = Regex.Split(str, delimiter);
if (lines.Length == 1)
continue;
for (int i = 0; i < lines.Length - 1; i++)
{
string strToSend = null;
if (IncludeDelimiter)
strToSend = lines[i] + delimiter;
else
strToSend = lines[i];
handler(this, new GenericCommMethodReceiveTextArgs(strToSend, delimiter));
}
ReceiveBuffer = new StringBuilder(lines[lines.Length - 1]);
}
}
}
/// <summary>
/// Deconstructor. Disconnects from port TextReceived events.
/// </summary>
~CommunicationGather()
{
Stop();
}
}
}

View File

@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using PepperDash.Core;
namespace PepperDash.Core
{
/// <summary>
/// Controls the ability to disable/enable debugging of TX/RX data sent to/from a device with a built in timer to disable
/// </summary>
public class CommunicationStreamDebugging
{
/// <summary>
/// Device Key that this instance configures
/// </summary>
public string ParentDeviceKey { get; private set; }
/// <summary>
/// Timer to disable automatically if not manually disabled
/// </summary>
private CTimer DebugExpiryPeriod;
/// <summary>
/// The current debug setting
/// </summary>
public eStreamDebuggingSetting DebugSetting { get; private set; }
private uint _DebugTimeoutInMs;
private const uint _DefaultDebugTimeoutMin = 30;
/// <summary>
/// Timeout in Minutes
/// </summary>
public uint DebugTimeoutMinutes
{
get
{
return _DebugTimeoutInMs/60000;
}
}
/// <summary>
/// Indicates that receive stream debugging is enabled
/// </summary>
public bool RxStreamDebuggingIsEnabled{ get; private set; }
/// <summary>
/// Indicates that transmit stream debugging is enabled
/// </summary>
public bool TxStreamDebuggingIsEnabled { get; private set; }
/// <summary>
/// Constructor
/// </summary>
/// <param name="parentDeviceKey"></param>
public CommunicationStreamDebugging(string parentDeviceKey)
{
ParentDeviceKey = parentDeviceKey;
}
/// <summary>
/// Sets the debugging setting and if not setting to off, assumes the default of 30 mintues
/// </summary>
/// <param name="setting"></param>
public void SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting setting)
{
if (setting == eStreamDebuggingSetting.Off)
{
DisableDebugging();
return;
}
SetDebuggingWithSpecificTimeout(setting, _DefaultDebugTimeoutMin);
}
/// <summary>
/// Sets the debugging setting for the specified number of minutes
/// </summary>
/// <param name="setting"></param>
/// <param name="minutes"></param>
public void SetDebuggingWithSpecificTimeout(eStreamDebuggingSetting setting, uint minutes)
{
if (setting == eStreamDebuggingSetting.Off)
{
DisableDebugging();
return;
}
_DebugTimeoutInMs = minutes * 60000;
StopDebugTimer();
DebugExpiryPeriod = new CTimer((o) => DisableDebugging(), _DebugTimeoutInMs);
if ((setting & eStreamDebuggingSetting.Rx) == eStreamDebuggingSetting.Rx)
RxStreamDebuggingIsEnabled = true;
if ((setting & eStreamDebuggingSetting.Tx) == eStreamDebuggingSetting.Tx)
TxStreamDebuggingIsEnabled = true;
Debug.SetDeviceDebugSettings(ParentDeviceKey, setting);
}
/// <summary>
/// Disabled debugging
/// </summary>
private void DisableDebugging()
{
StopDebugTimer();
Debug.SetDeviceDebugSettings(ParentDeviceKey, eStreamDebuggingSetting.Off);
}
private void StopDebugTimer()
{
RxStreamDebuggingIsEnabled = false;
TxStreamDebuggingIsEnabled = false;
if (DebugExpiryPeriod == null)
{
return;
}
DebugExpiryPeriod.Stop();
DebugExpiryPeriod.Dispose();
DebugExpiryPeriod = null;
}
}
/// <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
}
/// <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

@@ -0,0 +1,93 @@
using System;
using Crestron.SimplSharp;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace PepperDash.Core
{
/// <summary>
/// Config properties that indicate how to communicate with a device for control
/// </summary>
public class ControlPropertiesConfig
{
/// <summary>
/// The method of control
/// </summary>
[JsonProperty("method")]
[JsonConverter(typeof(StringEnumConverter))]
public eControlMethod Method { get; set; }
/// <summary>
/// The key of the device that contains the control port
/// </summary>
[JsonProperty("controlPortDevKey", NullValueHandling = NullValueHandling.Ignore)]
public string ControlPortDevKey { get; set; }
/// <summary>
/// The number of the control port on the device specified by ControlPortDevKey
/// </summary>
[JsonProperty("controlPortNumber", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value
public uint? ControlPortNumber { get; set; }
/// <summary>
/// The name of the control port on the device specified by ControlPortDevKey
/// </summary>
[JsonProperty("controlPortName", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value
public string ControlPortName { get; set; }
/// <summary>
/// Properties for ethernet based communications
/// </summary>
[JsonProperty("tcpSshProperties", NullValueHandling = NullValueHandling.Ignore)]
public TcpSshPropertiesConfig TcpSshProperties { get; set; }
/// <summary>
/// The filename and path for the IR file
/// </summary>
[JsonProperty("irFile", NullValueHandling = NullValueHandling.Ignore)]
public string IrFile { get; set; }
/// <summary>
/// The IpId of a Crestron device
/// </summary>
[JsonProperty("ipId", NullValueHandling = NullValueHandling.Ignore)]
public string IpId { get; set; }
/// <summary>
/// Readonly uint representation of the IpId
/// </summary>
[JsonIgnore]
public uint IpIdInt { get { return Convert.ToUInt32(IpId, 16); } }
/// <summary>
/// Char indicating end of line
/// </summary>
[JsonProperty("endOfLineChar", NullValueHandling = NullValueHandling.Ignore)]
public char EndOfLineChar { get; set; }
/// <summary>
/// Defaults to Environment.NewLine;
/// </summary>
[JsonProperty("endOfLineString", NullValueHandling = NullValueHandling.Ignore)]
public string EndOfLineString { get; set; }
/// <summary>
/// Indicates
/// </summary>
[JsonProperty("deviceReadyResponsePattern", NullValueHandling = NullValueHandling.Ignore)]
public string DeviceReadyResponsePattern { get; set; }
/// <summary>
/// Used when communcating to programs running in VC-4
/// </summary>
[JsonProperty("roomId", NullValueHandling = NullValueHandling.Ignore)]
public string RoomId { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ControlPropertiesConfig()
{
}
}
}

View File

@@ -0,0 +1,251 @@
/*PepperDash Technology Corp.
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
namespace PepperDash.Core
{
/// <summary>
/// Delegate for notifying of socket status changes
/// </summary>
/// <param name="client"></param>
public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client);
/// <summary>
/// EventArgs class for socket status changes
/// </summary>
public class GenericSocketStatusChageEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ISocketStatus Client { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="client"></param>
public GenericSocketStatusChageEventArgs(ISocketStatus client)
{
Client = client;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericSocketStatusChageEventArgs() { }
}
/// <summary>
/// Delegate for notifying of TCP Server state changes
/// </summary>
/// <param name="state"></param>
public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state);
/// <summary>
/// EventArgs class for TCP Server state changes
/// </summary>
public class GenericTcpServerStateChangedEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ServerState State { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="state"></param>
public GenericTcpServerStateChangedEventArgs(ServerState state)
{
State = state;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerStateChangedEventArgs() { }
}
/// <summary>
/// Delegate for TCP Server socket status changes
/// </summary>
/// <param name="socket"></param>
/// <param name="clientIndex"></param>
/// <param name="clientStatus"></param>
public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus);
/// <summary>
/// EventArgs for TCP server socket status changes
/// </summary>
public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public object Socket { get; private set; }
/// <summary>
///
/// </summary>
public uint ReceivedFromClientIndex { get; private set; }
/// <summary>
///
/// </summary>
public SocketStatus ClientStatus { get; set; }
/// <summary>
///
/// </summary>
/// <param name="socket"></param>
/// <param name="clientStatus"></param>
public GenericTcpServerSocketStatusChangeEventArgs(object socket, SocketStatus clientStatus)
{
Socket = socket;
ClientStatus = clientStatus;
}
/// <summary>
///
/// </summary>
/// <param name="socket"></param>
/// <param name="clientIndex"></param>
/// <param name="clientStatus"></param>
public GenericTcpServerSocketStatusChangeEventArgs(object socket, uint clientIndex, SocketStatus clientStatus)
{
Socket = socket;
ReceivedFromClientIndex = clientIndex;
ClientStatus = clientStatus;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerSocketStatusChangeEventArgs() { }
}
/// <summary>
/// EventArgs for TCP server com method receive text
/// </summary>
public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
{
/// <summary>
///
/// </summary>
public uint ReceivedFromClientIndex { get; private set; }
/// <summary>
///
/// </summary>
public ushort ReceivedFromClientIndexShort
{
get
{
return (ushort)ReceivedFromClientIndex;
}
}
/// <summary>
///
/// </summary>
public string Text { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
public GenericTcpServerCommMethodReceiveTextArgs(string text)
{
Text = text;
}
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="clientIndex"></param>
public GenericTcpServerCommMethodReceiveTextArgs(string text, uint clientIndex)
{
Text = text;
ReceivedFromClientIndex = clientIndex;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerCommMethodReceiveTextArgs() { }
}
/// <summary>
/// EventArgs for TCP server client ready for communication
/// </summary>
public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public bool IsReady;
/// <summary>
///
/// </summary>
/// <param name="isReady"></param>
public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady)
{
IsReady = isReady;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericTcpServerClientReadyForcommunicationsEventArgs() { }
}
/// <summary>
/// EventArgs for UDP connected
/// </summary>
public class GenericUdpConnectedEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ushort UConnected;
/// <summary>
///
/// </summary>
public bool Connected;
/// <summary>
/// Constructor
/// </summary>
public GenericUdpConnectedEventArgs() { }
/// <summary>
///
/// </summary>
/// <param name="uconnected"></param>
public GenericUdpConnectedEventArgs(ushort uconnected)
{
UConnected = uconnected;
}
/// <summary>
///
/// </summary>
/// <param name="connected"></param>
public GenericUdpConnectedEventArgs(bool connected)
{
Connected = connected;
}
}
}

View File

@@ -0,0 +1,955 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
namespace PepperDash.Core
{
/// <summary>
/// A class to handle secure TCP/IP communications with a server
/// </summary>
public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SplusKey = "Uninitialized Secure Tcp _client";
/// <summary>
/// Stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Fires when data is received from the server and returns it as a Byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Fires when data is received from the server and returns it as text
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
#region GenericSecureTcpIpClient Events & Delegates
/// <summary>
///
/// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
/// Auto reconnect evant handler
/// </summary>
public event EventHandler AutoReconnectTriggered;
/// <summary>
/// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread.
/// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event.
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceivedQueueInvoke;
/// <summary>
/// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require
/// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region GenricTcpIpClient properties
private string _hostname;
/// <summary>
/// Address of server
/// </summary>
public string Hostname
{
get { return _hostname; }
set
{
_hostname = value;
if (_client != null)
{
_client.AddressClientConnectedTo = _hostname;
}
}
}
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Internal secure client
/// </summary>
private SecureTCPClient _client;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// _client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// Connection failure reason
/// </summary>
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
///
/// </summary>
public bool Connected
{
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
// private Timer for auto reconnect
private CTimer RetryTimer;
#endregion
#region GenericSecureTcpIpClient properties
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Bool Heartbeat Enabled flag
/// </summary>
public bool HeartbeatEnabled { get; set; }
/// <summary>
/// S+ helper for Heartbeat Enabled
/// </summary>
public ushort UHeartbeatEnabled
{
get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
set { HeartbeatEnabled = value == 1; }
}
/// <summary>
/// Heartbeat String
/// </summary>
public string HeartbeatString { get; set; }
//public int HeartbeatInterval = 50000;
/// <summary>
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary>
public int HeartbeatInterval { get; set; }
/// <summary>
/// Simpl+ Heartbeat Analog value in seconds
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
// Used to force disconnection on a dead connect attempt
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
bool ProgramIsStopping;
/// <summary>
/// Queue lock
/// </summary>
CCriticalSection DequeueLock = new CCriticalSection();
/// <summary>
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
public int ReceiveQueueSize { get; set; }
/// <summary>
/// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
private CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs> MessageQueue;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericSecureTcpIpClient(string key, string address, int port, int bufferSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
Hostname = address;
Port = port;
BufferSize = bufferSize;
AutoReconnectIntervalMs = 5000;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
}
/// <summary>
/// Contstructor that sets all properties by calling the initialize method with a config object.
/// </summary>
/// <param name="key"></param>
/// <param name="clientConfigObject"></param>
public GenericSecureTcpIpClient(string key, TcpClientConfigObject clientConfigObject)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
Initialize(clientConfigObject);
}
/// <summary>
/// Default constructor for S+
/// </summary>
public GenericSecureTcpIpClient()
: base(SplusKey)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
}
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
/// </summary>
/// <param name="config"></param>
public void Initialize(TcpClientConfigObject config)
{
if (config == null)
{
Debug.Console(0, this, "Could not initialize client with key: {0}", Key);
return;
}
try
{
Hostname = config.Control.TcpSshProperties.Address;
Port = config.Control.TcpSshProperties.Port > 0 && config.Control.TcpSshProperties.Port <= 65535
? config.Control.TcpSshProperties.Port
: 80;
AutoReconnect = config.Control.TcpSshProperties.AutoReconnect;
AutoReconnectIntervalMs = config.Control.TcpSshProperties.AutoReconnectIntervalMs > 1000
? config.Control.TcpSshProperties.AutoReconnectIntervalMs
: 5000;
SharedKey = config.SharedKey;
SharedKeyRequired = config.SharedKeyRequired;
HeartbeatEnabled = config.HeartbeatRequired;
HeartbeatRequiredIntervalInSeconds = config.HeartbeatRequiredIntervalInSeconds > 0
? config.HeartbeatRequiredIntervalInSeconds
: (ushort)15;
HeartbeatString = string.IsNullOrEmpty(config.HeartbeatStringToMatch)
? "heartbeat"
: config.HeartbeatStringToMatch;
BufferSize = config.Control.TcpSshProperties.BufferSize > 2000
? config.Control.TcpSshProperties.BufferSize
: 2000;
ReceiveQueueSize = config.ReceiveQueueSize > 20
? config.ReceiveQueueSize
: 20;
MessageQueue = new CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs>(ReceiveQueueSize);
}
catch (Exception ex)
{
Debug.Console(0, this, "Exception initializing client with key: {0}\rException: {1}", Key, ex);
}
}
#endregion
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing _client connection");
ProgramIsStopping = true;
Disconnect();
}
}
/// <summary>
/// Deactivate the client
/// </summary>
/// <returns></returns>
public override bool Deactivate()
{
if (_client != null)
{
_client.SocketStatusChange -= this.Client_SocketStatusChange;
DisconnectClient();
}
return true;
}
/// <summary>
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary>
public void Connect()
{
ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
if (IsConnected)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
return;
}
if (IsTryingToConnect)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
return;
}
try
{
IsTryingToConnect = true;
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
return;
}
if (Port < 1 || Port > 65535)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
return;
}
// clean up previous client
if (_client != null)
{
Disconnect();
}
DisconnectCalledByUser = false;
_client = new SecureTCPClient(Hostname, Port, BufferSize);
_client.SocketStatusChange += Client_SocketStatusChange;
if (HeartbeatEnabled)
_client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5);
_client.AddressClientConnectedTo = Hostname;
_client.PortNumber = Port;
// SecureClient = c;
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect)
{
IsTryingToConnect = false;
//if (ConnectionHasHungCallback != null)
//{
// ConnectionHasHungCallback();
//}
//SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect();
}
}, 30000);
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
_client.ConnectToServerAsync(o =>
{
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
}
IsTryingToConnect = false;
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(2, this, "_client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive);
if (SharedKeyRequired)
{
WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer();
//CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event
}, 15000);
}
else
{
//CLient connected and shared key is not required so just raise the ready for communication event. if Shared key
//required this is called by the shared key being negotiated
if (IsReadyForCommunication == false)
{
OnClientReadyForcommunications(true); // Key not required
}
}
}
else
{
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect();
}
});
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "_client connection exception: {0}", ex.Message);
IsTryingToConnect = false;
CheckClosedAndTryReconnect();
}
}
/// <summary>
///
/// </summary>
public void Disconnect()
{
this.LogVerbose("Disconnect Called");
DisconnectCalledByUser = true;
// stop trying reconnects, if we are
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (_client != null)
{
DisconnectClient();
this.LogDebug("Disconnected");
}
}
/// <summary>
/// Does the actual disconnect business
/// </summary>
public void DisconnectClient()
{
if (_client == null) return;
Debug.Console(1, this, "Disconnecting client");
if (IsConnected)
_client.DisconnectFromServer();
// close up client. ALWAYS use this when disconnecting.
IsTryingToConnect = false;
Debug.Console(2, this, "Disconnecting _client {0}", DisconnectCalledByUser ? ", Called by user" : "");
_client.SocketStatusChange -= Client_SocketStatusChange;
_client.Dispose();
_client = null;
if (ConnectFailTimer == null) return;
ConnectFailTimer.Stop();
ConnectFailTimer.Dispose();
ConnectFailTimer = null;
}
#region Methods
/// <summary>
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
void CheckClosedAndTryReconnect()
{
if (_client != null)
{
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
Disconnect();
}
if (!DisconnectCalledByUser && AutoReconnect)
{
var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (AutoReconnectTriggered != null)
AutoReconnectTriggered(this, new EventArgs());
RetryTimer = new CTimer(o => Connect(), rndTime);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(SecureTCPClient client, int numBytes)
{
if (numBytes > 0)
{
string str = string.Empty;
var handler = TextReceivedQueueInvoke;
try
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "_client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{
if (SharedKeyRequired && str == "SharedKey:")
{
Debug.Console(2, this, "Server asking for shared key, sending");
SendText(SharedKey + "\n");
}
else if (SharedKeyRequired && str == "Shared Key Match")
{
StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange
}
else
{
//var bytesHandler = BytesReceived;
//if (bytesHandler != null)
// bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
var textHandler = TextReceived;
if (textHandler != null)
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
if (handler != null)
{
MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(str));
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
}
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
client.ReceiveDataAsync(Receive);
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null)
{
var gotLock = DequeueLock.TryEnter();
if (gotLock)
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
}
}
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
{
client.DisconnectFromServer();
}
}
/// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically.
/// </summary>
void DequeueEvent()
{
try
{
while (true)
{
// Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather.
var message = MessageQueue.Dequeue();
var handler = TextReceivedQueueInvoke;
if (handler != null)
{
handler(this, message);
}
}
}
catch (Exception e)
{
this.LogException(e, "DequeueEvent error");
}
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
if (DequeueLock != null)
{
DequeueLock.Leave();
}
}
void HeartbeatStart()
{
if (HeartbeatEnabled)
{
Debug.Console(2, this, "Starting Heartbeat");
if (HeartbeatSendTimer == null)
{
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
}
if (HeartbeatAckTimer == null)
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
}
}
void HeartbeatStop()
{
if (HeartbeatSendTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Send");
HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null;
}
if (HeartbeatAckTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Ack");
HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null;
}
}
void SendHeartbeat(object notused)
{
this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat");
}
//private method to check heartbeat requirements and start or reset timer
string checkHeartbeat(string received)
{
try
{
if (HeartbeatEnabled)
{
if (!string.IsNullOrEmpty(HeartbeatString))
{
var remainingText = received.Replace(HeartbeatString, "");
var noDelimiter = received.Trim(new char[] { '\r', '\n' });
if (noDelimiter.Contains(HeartbeatString))
{
if (HeartbeatAckTimer != null)
{
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
}
else
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText;
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
}
return received;
}
void HeartbeatAckTimerFail(object o)
{
try
{
if (IsConnected)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
ErrorLog.Error("Heartbeat timeout Error on _client: {0}, {1}", Key, ex);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// General send method
/// </summary>
public void SendText(string text)
{
if (!string.IsNullOrEmpty(text))
{
try
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
_client.SendDataAsync(bytes, bytes.Length, (c, n) =>
{
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
}
});
}
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
}
}
}
/// <summary>
///
/// </summary>
public void SendBytes(byte[] bytes)
{
if (bytes.Length > 0)
{
try
{
if (_client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
_client.SendData(bytes, bytes.Length);
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus)
{
if (ProgramIsStopping)
{
ProgramIsStopping = false;
return;
}
try
{
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange();
// The client could be null or disposed by this time...
if (_client == null || _client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
HeartbeatStop();
OnClientReadyForcommunications(false); // socket has gone low
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler == null) return;
handler(this, new GenericSocketStatusChageEventArgs(this));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (IsReadyForCommunication)
HeartbeatStart();
var handler = ClientReadyForCommunications;
if (handler == null) return;
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

View File

@@ -0,0 +1,909 @@
/*PepperDash Technology Corp.
JAG
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using PepperDash.Core.Logging;
namespace PepperDash.Core
{
/// <summary>
/// Generic secure TCP/IP client for server
/// </summary>
public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
{
/// <summary>
/// Band aid delegate for choked server
/// </summary>
internal delegate void ConnectionHasHungCallbackDelegate();
#region Events
//public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Notifies of text received
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Notifies of auto reconnect sequence triggered
/// </summary>
public event EventHandler AutoReconnectTriggered;
/// <summary>
/// Event for Receiving text. Once subscribed to this event the receive callback will start a thread that dequeues the messages and invokes the event on a new thread.
/// It is not recommended to use both the TextReceived event and the TextReceivedQueueInvoke event.
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceivedQueueInvoke;
/// <summary>
/// Notifies of socket status change
/// </summary>
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ConnectionChange;
/// <summary>
/// This is something of a band-aid callback. If the client times out during the connection process, because the server
/// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help
/// keep a watch on the server and reset it if necessary.
/// </summary>
internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
/// <summary>
/// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require
/// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region Properties & Variables
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
if (Client != null)
return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
else
return false;
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
if (Client != null)
return Client.ClientStatus;
else
return SocketStatus.SOCKET_STATUS_NO_CONNECT;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// private Timer for auto reconnect
/// </summary>
CTimer RetryTimer;
/// <summary>
///
/// </summary>
public bool HeartbeatEnabled { get; set; }
/// <summary>
///
/// </summary>
public ushort UHeartbeatEnabled
{
get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
set { HeartbeatEnabled = value == 1; }
}
/// <summary>
///
/// </summary>
public string HeartbeatString { get; set; }
//public int HeartbeatInterval = 50000;
/// <summary>
/// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
/// </summary>
public int HeartbeatInterval { get; set; }
/// <summary>
/// Simpl+ Heartbeat Analog value in seconds
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatInterval = (value * 1000); } }
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
/// </summary>
SecureTCPClient Client;
bool ProgramIsStopping;
/// <summary>
/// Queue lock
/// </summary>
CCriticalSection DequeueLock = new CCriticalSection();
/// <summary>
/// Receive Queue size. Defaults to 20. Will set to 20 if QueueSize property is less than 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
public int ReceiveQueueSize { get; set; }
/// <summary>
/// Queue to temporarily store received messages with the source IP and Port info. Defaults to size 20. Use constructor or set queue size property before
/// calling initialize.
/// </summary>
private CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs> MessageQueue;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericSecureTcpIpClient_ForServer(string key, string address, int port, int bufferSize)
: base(key)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Hostname = address;
Port = port;
BufferSize = bufferSize;
AutoReconnectIntervalMs = 5000;
}
/// <summary>
/// Constructor for S+
/// </summary>
public GenericSecureTcpIpClient_ForServer()
: base("Uninitialized Secure Tcp Client For Server")
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
}
/// <summary>
/// Contstructor that sets all properties by calling the initialize method with a config object.
/// </summary>
/// <param name="key"></param>
/// <param name="clientConfigObject"></param>
public GenericSecureTcpIpClient_ForServer(string key, TcpClientConfigObject clientConfigObject)
: base(key)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Initialize(clientConfigObject);
}
#endregion
#region Methods
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
/// </summary>
/// <param name="clientConfigObject"></param>
public void Initialize(TcpClientConfigObject clientConfigObject)
{
try
{
if (clientConfigObject != null)
{
var TcpSshProperties = clientConfigObject.Control.TcpSshProperties;
Hostname = TcpSshProperties.Address;
AutoReconnect = TcpSshProperties.AutoReconnect;
AutoReconnectIntervalMs = TcpSshProperties.AutoReconnectIntervalMs > 1000 ?
TcpSshProperties.AutoReconnectIntervalMs : 5000;
SharedKey = clientConfigObject.SharedKey;
SharedKeyRequired = clientConfigObject.SharedKeyRequired;
HeartbeatEnabled = clientConfigObject.HeartbeatRequired;
HeartbeatRequiredIntervalInSeconds = clientConfigObject.HeartbeatRequiredIntervalInSeconds > 0 ?
clientConfigObject.HeartbeatRequiredIntervalInSeconds : (ushort)15;
HeartbeatString = string.IsNullOrEmpty(clientConfigObject.HeartbeatStringToMatch) ? "heartbeat" : clientConfigObject.HeartbeatStringToMatch;
Port = TcpSshProperties.Port;
BufferSize = TcpSshProperties.BufferSize > 2000 ? TcpSshProperties.BufferSize : 2000;
ReceiveQueueSize = clientConfigObject.ReceiveQueueSize > 20 ? clientConfigObject.ReceiveQueueSize : 20;
MessageQueue = new CrestronQueue<GenericTcpServerCommMethodReceiveTextArgs>(ReceiveQueueSize);
}
else
{
ErrorLog.Error("Could not initialize client with key: {0}", Key);
}
}
catch
{
ErrorLog.Error("Could not initialize client with key: {0}", Key);
}
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection");
ProgramIsStopping = true;
Disconnect();
}
}
/// <summary>
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary>
public void Connect()
{
ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
if (IsConnected)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
return;
}
if (IsTryingToConnect)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
return;
}
try
{
IsTryingToConnect = true;
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
return;
}
if (Port < 1 || Port > 65535)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
return;
}
// clean up previous client
if (Client != null)
{
Cleanup();
}
DisconnectCalledByUser = false;
Client = new SecureTCPClient(Hostname, Port, BufferSize);
Client.SocketStatusChange += Client_SocketStatusChange;
if (HeartbeatEnabled)
Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5);
Client.AddressClientConnectedTo = Hostname;
Client.PortNumber = Port;
// SecureClient = c;
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect)
{
IsTryingToConnect = false;
//if (ConnectionHasHungCallback != null)
//{
// ConnectionHasHungCallback();
//}
//SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect();
}
}, 30000);
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
Client.ConnectToServerAsync(o =>
{
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
}
IsTryingToConnect = false;
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive);
if (SharedKeyRequired)
{
WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer();
//CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event
}, 15000);
}
else
{
//CLient connected and shared key is not required so just raise the ready for communication event. if Shared key
//required this is called by the shared key being negotiated
if (IsReadyForCommunication == false)
{
OnClientReadyForcommunications(true); // Key not required
}
}
}
else
{
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect();
}
});
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message);
IsTryingToConnect = false;
CheckClosedAndTryReconnect();
}
}
/// <summary>
///
/// </summary>
public void Disconnect()
{
this.LogVerbose("Disconnect Called");
DisconnectCalledByUser = true;
if (IsConnected)
{
Client.DisconnectFromServer();
}
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
Cleanup();
}
/// <summary>
/// Internal call to close up client. ALWAYS use this when disconnecting.
/// </summary>
void Cleanup()
{
IsTryingToConnect = false;
if (Client != null)
{
//SecureClient.DisconnectFromServer();
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
Client.SocketStatusChange -= Client_SocketStatusChange;
Client.Dispose();
Client = null;
}
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
ConnectFailTimer.Dispose();
ConnectFailTimer = null;
}
}
/// <summary>ff
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
void CheckClosedAndTryReconnect()
{
if (Client != null)
{
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
Cleanup();
}
if (!DisconnectCalledByUser && AutoReconnect)
{
var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if(AutoReconnectTriggered != null)
AutoReconnectTriggered(this, new EventArgs());
RetryTimer = new CTimer(o => Connect(), rndTime);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(SecureTCPClient client, int numBytes)
{
if (numBytes > 0)
{
string str = string.Empty;
var handler = TextReceivedQueueInvoke;
try
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{
if (SharedKeyRequired && str == "SharedKey:")
{
Debug.Console(2, this, "Server asking for shared key, sending");
SendText(SharedKey + "\n");
}
else if (SharedKeyRequired && str == "Shared Key Match")
{
StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange
}
else
{
//var bytesHandler = BytesReceived;
//if (bytesHandler != null)
// bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
var textHandler = TextReceived;
if (textHandler != null)
textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str));
if (handler != null)
{
MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(str));
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
}
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
client.ReceiveDataAsync(Receive);
//Check to see if there is a subscription to the TextReceivedQueueInvoke event. If there is start the dequeue thread.
if (handler != null)
{
var gotLock = DequeueLock.TryEnter();
if (gotLock)
CrestronInvoke.BeginInvoke((o) => DequeueEvent());
}
}
else //JAG added this as I believe the error return is 0 bytes like the server. See help when hover on ReceiveAsync
{
client.DisconnectFromServer();
}
}
/// <summary>
/// This method gets spooled up in its own thread an protected by a CCriticalSection to prevent multiple threads from running concurrently.
/// It will dequeue items as they are enqueued automatically.
/// </summary>
void DequeueEvent()
{
try
{
while (true)
{
// Pull from Queue and fire an event. Block indefinitely until an item can be removed, similar to a Gather.
var message = MessageQueue.Dequeue();
var handler = TextReceivedQueueInvoke;
if (handler != null)
{
handler(this, message);
}
}
}
catch (Exception e)
{
this.LogException(ex, "DequeueEvent error");
}
// Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it.
if (DequeueLock != null)
{
DequeueLock.Leave();
}
}
void HeartbeatStart()
{
if (HeartbeatEnabled)
{
Debug.Console(2, this, "Starting Heartbeat");
if (HeartbeatSendTimer == null)
{
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
}
if (HeartbeatAckTimer == null)
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
}
}
void HeartbeatStop()
{
if (HeartbeatSendTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Send");
HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null;
}
if (HeartbeatAckTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Ack");
HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null;
}
}
void SendHeartbeat(object notused)
{
this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat");
}
//private method to check heartbeat requirements and start or reset timer
string checkHeartbeat(string received)
{
try
{
if (HeartbeatEnabled)
{
if (!string.IsNullOrEmpty(HeartbeatString))
{
var remainingText = received.Replace(HeartbeatString, "");
var noDelimiter = received.Trim(new char[] { '\r', '\n' });
if (noDelimiter.Contains(HeartbeatString))
{
if (HeartbeatAckTimer != null)
{
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
}
else
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText;
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
}
return received;
}
void HeartbeatAckTimerFail(object o)
{
try
{
if (IsConnected)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// General send method
/// </summary>
public void SendText(string text)
{
if (!string.IsNullOrEmpty(text))
{
try
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Client.SendDataAsync(bytes, bytes.Length, (c, n) =>
{
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
}
});
}
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
}
}
}
/// <summary>
///
/// </summary>
public void SendBytes(byte[] bytes)
{
if (bytes.Length > 0)
{
try
{
if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
Client.SendData(bytes, bytes.Length);
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(SecureTCPClient client, SocketStatus clientSocketStatus)
{
if (ProgramIsStopping)
{
ProgramIsStopping = false;
return;
}
try
{
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange();
// The client could be null or disposed by this time...
if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
HeartbeatStop();
OnClientReadyForcommunications(false); // socket has gone low
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (this.IsReadyForCommunication) { HeartbeatStart(); }
var handler = ClientReadyForCommunications;
if (handler != null)
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,592 @@
using System;
using System.Text;
using System.Threading;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Org.BouncyCastle.Utilities;
using PepperDash.Core.Logging;
using Renci.SshNet;
using Renci.SshNet.Common;
namespace PepperDash.Core
{
/// <summary>
///
/// </summary>
public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SPlusKey = "Uninitialized SshClient";
/// <summary>
/// Object to enable stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Event that fires when data is received. Delivers args with byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Event that fires when data is received. Delivered as text.
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Event when the connection status changes.
/// </summary>
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
///// <summary>
/////
///// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// Username for server
/// </summary>
public string Username { get; set; }
/// <summary>
/// And... Password for server. That was worth documenting!
/// </summary>
public string Password { get; set; }
/// <summary>
/// True when the server is connected - when status == 2.
/// </summary>
public bool IsConnected
{
// returns false if no client or not connected
get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
///
/// </summary>
public SocketStatus ClientStatus
{
get { return _ClientStatus; }
private set
{
if (_ClientStatus == value)
return;
_ClientStatus = value;
OnConnectionChange();
}
}
SocketStatus _ClientStatus;
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected with be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)_ClientStatus; }
}
/// <summary>
/// Determines whether client will attempt reconnection on failure. Default is true
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// Will be set and unset by connect and disconnect only
/// </summary>
public bool ConnectEnabled { get; private set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Millisecond value, determines the timeout period in between reconnect attempts.
/// Set to 5000 by default
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
SshClient Client;
ShellStream TheStream;
CTimer ReconnectTimer;
//Lock object to prevent simulatneous connect/disconnect operations
//private CCriticalSection connectLock = new CCriticalSection();
private SemaphoreSlim connectLock = new SemaphoreSlim(1);
private bool DisconnectLogged = false;
/// <summary>
/// Typical constructor.
/// </summary>
public GenericSshClient(string key, string hostname, int port, string username, string password) :
base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Key = key;
Hostname = hostname;
Port = port;
Username = username;
Password = password;
AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o =>
{
if (ConnectEnabled)
{
Connect();
}
}, System.Threading.Timeout.Infinite);
}
/// <summary>
/// S+ Constructor - Must set all properties before calling Connect
/// </summary>
public GenericSshClient()
: base(SPlusKey)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
ReconnectTimer = new CTimer(o =>
{
if (ConnectEnabled)
{
Connect();
}
}, System.Threading.Timeout.Infinite);
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
if (Client != null)
{
this.LogDebug("Program stopping. Closing connection");
Disconnect();
}
}
}
/// <summary>
/// Connect to the server, using the provided properties.
/// </summary>
public void Connect()
{
// Don't go unless everything is here
if (string.IsNullOrEmpty(Hostname) || Port < 1 || Port > 65535
|| Username == null || Password == null)
{
this.LogError("Connect failed. Check hostname, port, username and password are set or not null");
return;
}
ConnectEnabled = true;
try
{
connectLock.Wait();
if (IsConnected)
{
this.LogDebug("Connection already connected. Exiting Connect");
}
else
{
this.LogDebug("Attempting connect");
// Cancel reconnect if running.
if (ReconnectTimer != null)
{
ReconnectTimer.Stop();
}
// Cleanup the old client if it already exists
if (Client != null)
{
this.LogDebug("Cleaning up disconnected client");
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
}
// This handles both password and keyboard-interactive (like on OS-X, 'nixes)
KeyboardInteractiveAuthenticationMethod kauth = new KeyboardInteractiveAuthenticationMethod(Username);
kauth.AuthenticationPrompt += new EventHandler<AuthenticationPromptEventArgs>(kauth_AuthenticationPrompt);
PasswordAuthenticationMethod pauth = new PasswordAuthenticationMethod(Username, Password);
this.LogDebug("Creating new SshClient");
ConnectionInfo connectionInfo = new ConnectionInfo(Hostname, Port, Username, pauth, kauth);
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)
{
// empty the buffer if there is data
string str = TheStream.Read();
}
TheStream.DataReceived += Stream_DataReceived;
this.LogInformation("Connected");
ClientStatus = SocketStatus.SOCKET_STATUS_CONNECTED;
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)
{
this.LogException(ie, "CONNECTION failure: Cannot reach host");
}
if (ie is System.Net.Sockets.SocketException socketException)
{
this.LogException(ie, "Connection failure: Cannot reach {host} on {port}",
Hostname, Port);
}
if (ie is SshAuthenticationException)
{
this.LogException(ie, "Authentication failure for username {userName}", Username);
}
else
this.LogException(ie, "Error on connect");
DisconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
}
}
catch(SshOperationTimeoutException ex)
{
this.LogWarning("Connection attempt timed out: {message}", ex.Message);
DisconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
}
}
catch (Exception e)
{
var errorLogLevel = DisconnectLogged == true ? Debug.ErrorLogLevel.None : Debug.ErrorLogLevel.Error;
this.LogException(e, "Unhandled exception on connect");
DisconnectLogged = true;
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
if (AutoReconnect)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
}
}
}
}
finally
{
connectLock.Release();
}
}
/// <summary>
/// Disconnect the clients and put away it's resources.
/// </summary>
public void Disconnect()
{
ConnectEnabled = false;
// Stop trying reconnects, if we are
if (ReconnectTimer != null)
{
ReconnectTimer.Stop();
// ReconnectTimer = null;
}
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
}
/// <summary>
/// Kills the stream, cleans up the client and sets it to null
/// </summary>
private void KillClient(SocketStatus status)
{
KillStream();
try
{
if (Client != null)
{
Client.ErrorOccurred -= Client_ErrorOccurred;
Client.Disconnect();
Client.Dispose();
Client = null;
ClientStatus = status;
this.LogDebug("Disconnected");
}
}
catch (Exception ex)
{
this.LogException(ex,"Exception in Kill Client");
}
}
/// <summary>
/// Kills the stream
/// </summary>
void KillStream()
{
try
{
if (TheStream != null)
{
TheStream.DataReceived -= Stream_DataReceived;
TheStream.Close();
TheStream.Dispose();
TheStream = null;
this.LogDebug("Disconnected stream");
}
}
catch (Exception ex)
{
this.LogException(ex, "Exception in Kill Stream:{0}");
}
}
/// <summary>
/// Handles the keyboard interactive authentication, should it be required.
/// </summary>
void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
{
foreach (AuthenticationPrompt prompt in e.Prompts)
if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
prompt.Response = Password;
}
/// <summary>
/// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
/// </summary>
void Stream_DataReceived(object sender, ShellDataEventArgs e)
{
if (((ShellStream)sender).Length <= 0L)
{
return;
}
var response = ((ShellStream)sender).Read();
var bytesHandler = BytesReceived;
if (bytesHandler != null)
{
var bytes = Encoding.UTF8.GetBytes(response);
if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
this.LogInformation("Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
}
var textHandler = TextReceived;
if (textHandler != null)
{
if (StreamDebugging.RxStreamDebuggingIsEnabled)
this.LogInformation("Received: '{0}'", ComTextHelper.GetDebugText(response));
textHandler(this, new GenericCommMethodReceiveTextArgs(response));
}
}
/// <summary>
/// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
/// event
/// </summary>
void Client_ErrorOccurred(object sender, ExceptionEventArgs e)
{
CrestronInvoke.BeginInvoke(o =>
{
if (e.Exception is SshConnectionException || e.Exception is System.Net.Sockets.SocketException)
this.LogError("Disconnected by remote");
else
this.LogException(e.Exception, "Unhandled SSH client error");
try
{
connectLock.Wait();
KillClient(SocketStatus.SOCKET_STATUS_BROKEN_REMOTELY);
}
finally
{
connectLock.Release();
}
if (AutoReconnect && ConnectEnabled)
{
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
ReconnectTimer.Reset(AutoReconnectIntervalMs);
}
});
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
if (ConnectionChange != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
}
#region IBasicCommunication Members
/// <summary>
/// Sends text to the server
/// </summary>
/// <param name="text"></param>
public void SendText(string text)
{
try
{
if (Client != null && TheStream != null && IsConnected)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
this.LogInformation(
"Sending {length} characters of text: '{text}'",
text.Length,
ComTextHelper.GetDebugText(text));
TheStream.Write(text);
TheStream.Flush();
}
else
{
this.LogDebug("Client is null or disconnected. Cannot Send Text");
}
}
catch (ObjectDisposedException)
{
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
ReconnectTimer.Reset();
}
catch (Exception ex)
{
this.LogException(ex, "Exception sending text: '{message}'", text);
}
}
/// <summary>
/// Sends Bytes to the server
/// </summary>
/// <param name="bytes"></param>
public void SendBytes(byte[] bytes)
{
try
{
if (Client != null && TheStream != null && IsConnected)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
this.LogInformation("Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
TheStream.Write(bytes, 0, bytes.Length);
TheStream.Flush();
}
else
{
this.LogDebug("Client is null or disconnected. Cannot Send Bytes");
}
}
catch (ObjectDisposedException ex)
{
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
ReconnectTimer.Reset();
}
catch (Exception ex)
{
this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes));
}
}
#endregion
}
//*****************************************************************************************************
//*****************************************************************************************************
/// <summary>
/// Fired when connection changes
/// </summary>
public class SshConnectionChangeEventArgs : EventArgs
{
/// <summary>
/// Connection State
/// </summary>
public bool IsConnected { get; private set; }
/// <summary>
/// Connection Status represented as a ushort
/// </summary>
public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } }
/// <summary>
/// The client
/// </summary>
public GenericSshClient Client { get; private set; }
/// <summary>
/// Socket Status as represented by
/// </summary>
public ushort Status { get { return Client.UStatus; } }
/// <summary>
/// S+ Constructor
/// </summary>
public SshConnectionChangeEventArgs() { }
/// <summary>
/// EventArgs class
/// </summary>
/// <param name="isConnected">Connection State</param>
/// <param name="client">The Client</param>
public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client)
{
IsConnected = isConnected;
Client = client;
}
}
}

View File

@@ -0,0 +1,566 @@
using System;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// A class to handle basic TCP/IP communications with a server
/// </summary>
public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
{
private const string SplusKey = "Uninitialized TcpIpClient";
/// <summary>
/// Object to enable stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
/// Fires when data is received from the server and returns it as a Byte array
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Fires when data is received from the server and returns it as text
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
///
/// </summary>
//public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
private string _hostname;
/// <summary>
/// Address of server
/// </summary>
public string Hostname
{
get
{
return _hostname;
}
set
{
_hostname = value;
if (_client != null)
{
_client.AddressClientConnectedTo = _hostname;
}
}
}
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// Another damn S+ helper because S+ seems to treat large port nums as signed ints
/// which screws up things
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// The actual client class
/// </summary>
private TCPClient _client;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// _client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// Ushort representation of client status
/// </summary>
[Obsolete]
public ushort UClientStatus { get { return (ushort)ClientStatus; } }
/// <summary>
/// Connection failure reason
/// </summary>
public string ConnectionFailure { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Set only when the disconnect method is called
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
///
/// </summary>
public bool Connected
{
get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
}
//Lock object to prevent simulatneous connect/disconnect operations
private CCriticalSection connectLock = new CCriticalSection();
// private Timer for auto reconnect
private CTimer RetryTimer;
/// <summary>
/// Constructor
/// </summary>
/// <param name="key">unique string to differentiate between instances</param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericTcpIpClient(string key, string address, int port, int bufferSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
Hostname = address;
Port = port;
BufferSize = bufferSize;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
public GenericTcpIpClient(string key)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
/// <summary>
/// Default constructor for S+
/// </summary>
public GenericTcpIpClient()
: base(SplusKey)
{
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
RetryTimer = new CTimer(o =>
{
Reconnect();
}, Timeout.Infinite);
}
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
Debug.Console(1, this, "Program stopping. Closing connection");
Deactivate();
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override bool Deactivate()
{
RetryTimer.Stop();
RetryTimer.Dispose();
if (_client != null)
{
_client.SocketStatusChange -= this.Client_SocketStatusChange;
DisconnectClient();
}
return true;
}
/// <summary>
/// Attempts to connect to the server
/// </summary>
public void Connect()
{
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': No address set", Key);
return;
}
if (Port < 1 || Port > 65535)
{
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericTcpIpClient '{0}': Invalid port", Key);
return;
}
}
try
{
connectLock.Enter();
if (IsConnected)
{
Debug.Console(1, this, "Connection already connected. Exiting Connect()");
}
else
{
//Stop retry timer if running
RetryTimer.Stop();
_client = new TCPClient(Hostname, Port, BufferSize);
_client.SocketStatusChange -= Client_SocketStatusChange;
_client.SocketStatusChange += Client_SocketStatusChange;
DisconnectCalledByUser = false;
_client.ConnectToServerAsync(ConnectToServerCallback);
}
}
finally
{
connectLock.Leave();
}
}
private void Reconnect()
{
if (_client == null)
{
return;
}
try
{
connectLock.Enter();
if (IsConnected || DisconnectCalledByUser == true)
{
Debug.Console(1, this, "Reconnect no longer needed. Exiting Reconnect()");
}
else
{
Debug.Console(1, this, "Attempting reconnect now");
_client.ConnectToServerAsync(ConnectToServerCallback);
}
}
finally
{
connectLock.Leave();
}
}
/// <summary>
/// Attempts to disconnect the client
/// </summary>
public void Disconnect()
{
try
{
connectLock.Enter();
DisconnectCalledByUser = true;
// Stop trying reconnects, if we are
RetryTimer.Stop();
DisconnectClient();
}
finally
{
connectLock.Leave();
}
}
/// <summary>
/// Does the actual disconnect business
/// </summary>
public void DisconnectClient()
{
if (_client != null)
{
Debug.Console(1, this, "Disconnecting client");
if (IsConnected)
_client.DisconnectFromServer();
}
}
/// <summary>
/// Callback method for connection attempt
/// </summary>
/// <param name="c"></param>
void ConnectToServerCallback(TCPClient c)
{
if (c.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(0, this, "Server connection result: {0}", c.ClientStatus);
WaitAndTryReconnect();
}
else
{
Debug.Console(1, this, "Server connection result: {0}", c.ClientStatus);
}
}
/// <summary>
/// Disconnects, waits and attemtps to connect again
/// </summary>
void WaitAndTryReconnect()
{
CrestronInvoke.BeginInvoke(o =>
{
try
{
connectLock.Enter();
if (!IsConnected && AutoReconnect && !DisconnectCalledByUser && _client != null)
{
DisconnectClient();
Debug.Console(1, this, "Attempting reconnect, status={0}", _client.ClientStatus);
RetryTimer.Reset(AutoReconnectIntervalMs);
}
}
finally
{
connectLock.Leave();
}
});
}
/// <summary>
/// Recieves incoming data
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(TCPClient client, int numBytes)
{
if (client != null)
{
if (numBytes > 0)
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
var bytesHandler = BytesReceived;
if (bytesHandler != null)
{
if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
}
var textHandler = TextReceived;
if (textHandler != null)
{
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
}
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
}
}
client.ReceiveDataAsync(Receive);
}
}
/// <summary>
/// General send method
/// </summary>
public void SendText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
// Check debug level before processing byte array
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
if (_client != null)
_client.SendData(bytes, bytes.Length);
}
/// <summary>
/// This is useful from console and...?
/// </summary>
public void SendEscapedText(string text)
{
var unescapedText = Regex.Replace(text, @"\\x([0-9a-fA-F][0-9a-fA-F])", s =>
{
var hex = s.Groups[1].Value;
return ((char)Convert.ToByte(hex, 16)).ToString();
});
SendText(unescapedText);
}
/// <summary>
/// Sends Bytes to the server
/// </summary>
/// <param name="bytes"></param>
public void SendBytes(byte[] bytes)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (_client != null)
_client.SendData(bytes, bytes.Length);
}
/// <summary>
/// Socket Status Change Handler
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
{
if (clientSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(0, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
WaitAndTryReconnect();
}
else
{
Debug.Console(1, this, "Socket status change {0} ({1})", clientSocketStatus, ClientStatusText);
_client.ReceiveDataAsync(Receive);
}
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
}
}
/// <summary>
/// Configuration properties for TCP/SSH Connections
/// </summary>
public class TcpSshPropertiesConfig
{
/// <summary>
/// Address to connect to
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Address { get; set; }
/// <summary>
/// Port to connect to
/// </summary>
[JsonProperty(Required = Required.Always)]
public int Port { get; set; }
/// <summary>
/// Username credential
/// </summary>
public string Username { get; set; }
/// <summary>
/// Passord credential
/// </summary>
public string Password { get; set; }
/// <summary>
/// Defaults to 32768
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Defaults to true
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// Defaults to 5000ms
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public TcpSshPropertiesConfig()
{
BufferSize = 32768;
AutoReconnect = true;
AutoReconnectIntervalMs = 5000;
Username = "";
Password = "";
}
}
}

View File

@@ -0,0 +1,774 @@
/*PepperDash Technology Corp.
JAG
Copyright: 2017
------------------------------------
***Notice of Ownership and Copyright***
The material in which this notice appears is the property of PepperDash Technology Corporation,
which claims copyright under the laws of the United States of America in the entire body of material
and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part,
of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited.
PepperDash Technology Corporation reserves all rights under applicable laws.
------------------------------------ */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
namespace PepperDash.Core
{
/// <summary>
/// Generic TCP/IP client for server
/// </summary>
public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
{
/// <summary>
/// Band aid delegate for choked server
/// </summary>
internal delegate void ConnectionHasHungCallbackDelegate();
#region Events
//public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Notifies of text received
/// </summary>
public event EventHandler<GenericTcpServerCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Notifies of socket status change
/// </summary>
public event EventHandler<GenericTcpServerSocketStatusChangeEventArgs> ConnectionChange;
/// <summary>
/// This is something of a band-aid callback. If the client times out during the connection process, because the server
/// is stuck, this will fire. It is intended to be used by the Server class monitor client, to help
/// keep a watch on the server and reset it if necessary.
/// </summary>
internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
/// <summary>
/// For a client with a pre shared key, this will fire after the communication is established and the key exchange is complete. If you require
/// a key and subscribe to the socket change event and try to send data on a connection the data sent will interfere with the key exchange and disconnect.
/// </summary>
public event EventHandler<GenericTcpServerClientReadyForcommunicationsEventArgs> ClientReadyForCommunications;
#endregion
#region Properties & Variables
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// S+ helper for requires shared key bool
/// </summary>
public ushort USharedKeyRequired
{
set
{
if (value == 1)
SharedKeyRequired = true;
else
SharedKeyRequired = false;
}
}
/// <summary>
/// SharedKey is sent for varification to the server. Shared key can be any text (255 char limit in SIMPL+ Module), but must match the Shared Key on the Server module
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// flag to show the client is waiting for the server to send the shared key
/// </summary>
private bool WaitingForSharedKeyResponse { get; set; }
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Semaphore on connect method
/// </summary>
bool IsTryingToConnect;
/// <summary>
/// Bool showing if socket is connected
/// </summary>
public bool IsConnected
{
get
{
if (Client != null)
return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
else
return false;
}
}
/// <summary>
/// S+ helper for IsConnected
/// </summary>
public ushort UIsConnected
{
get { return (ushort)(IsConnected ? 1 : 0); }
}
/// <summary>
/// Bool showing if socket is ready for communication after shared key exchange
/// </summary>
public bool IsReadyForCommunication { get; set; }
/// <summary>
/// S+ helper for IsReadyForCommunication
/// </summary>
public ushort UIsReadyForCommunication
{
get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
}
/// <summary>
/// Client socket status Read only
/// </summary>
public SocketStatus ClientStatus
{
get
{
if (Client != null)
return Client.ClientStatus;
else
return SocketStatus.SOCKET_STATUS_NO_CONNECT;
}
}
/// <summary>
/// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
/// and IsConnected would be true when this == 2.
/// </summary>
public ushort UStatus
{
get { return (ushort)ClientStatus; }
}
/// <summary>
/// Status text shows the message associated with socket status
/// </summary>
public string ClientStatusText { get { return ClientStatus.ToString(); } }
/// <summary>
/// bool to track if auto reconnect should be set on the socket
/// </summary>
public bool AutoReconnect { get; set; }
/// <summary>
/// S+ helper for AutoReconnect
/// </summary>
public ushort UAutoReconnect
{
get { return (ushort)(AutoReconnect ? 1 : 0); }
set { AutoReconnect = value == 1; }
}
/// <summary>
/// Milliseconds to wait before attempting to reconnect. Defaults to 5000
/// </summary>
public int AutoReconnectIntervalMs { get; set; }
/// <summary>
/// Flag Set only when the disconnect method is called.
/// </summary>
bool DisconnectCalledByUser;
/// <summary>
/// private Timer for auto reconnect
/// </summary>
CTimer RetryTimer;
/// <summary>
///
/// </summary>
public bool HeartbeatEnabled { get; set; }
/// <summary>
///
/// </summary>
public ushort UHeartbeatEnabled
{
get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
set { HeartbeatEnabled = value == 1; }
}
/// <summary>
///
/// </summary>
public string HeartbeatString = "heartbeat";
/// <summary>
///
/// </summary>
public int HeartbeatInterval = 50000;
CTimer HeartbeatSendTimer;
CTimer HeartbeatAckTimer;
/// <summary>
/// Used to force disconnection on a dead connect attempt
/// </summary>
CTimer ConnectFailTimer;
CTimer WaitForSharedKey;
private int ConnectionCount;
/// <summary>
/// Internal secure client
/// </summary>
TCPClient Client;
bool ProgramIsStopping;
#endregion
#region Constructors
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="bufferSize"></param>
public GenericTcpIpClient_ForServer(string key, string address, int port, int bufferSize)
: base(key)
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
Hostname = address;
Port = port;
BufferSize = bufferSize;
AutoReconnectIntervalMs = 5000;
}
/// <summary>
/// Constructor for S+
/// </summary>
public GenericTcpIpClient_ForServer()
: base("Uninitialized DynamicTcpClient")
{
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
AutoReconnectIntervalMs = 5000;
BufferSize = 2000;
}
#endregion
#region Methods
/// <summary>
/// Just to help S+ set the key
/// </summary>
public void Initialize(string key)
{
Key = key;
}
/// <summary>
/// Handles closing this up when the program shuts down
/// </summary>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping || programEventType == eProgramStatusEventType.Paused)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing Client connection");
ProgramIsStopping = true;
Disconnect();
}
}
/// <summary>
/// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
/// </summary>
public void Connect()
{
ConnectionCount++;
Debug.Console(2, this, "Attempting connect Count:{0}", ConnectionCount);
if (IsConnected)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already connected. Ignoring.");
return;
}
if (IsTryingToConnect)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Notice, "Already trying to connect. Ignoring.");
return;
}
try
{
IsTryingToConnect = true;
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No address set");
return;
}
if (Port < 1 || Port > 65535)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: Invalid port");
return;
}
if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Warning, "DynamicTcpClient: No Shared Key set");
return;
}
// clean up previous client
if (Client != null)
{
Cleanup();
}
DisconnectCalledByUser = false;
Client = new TCPClient(Hostname, Port, BufferSize);
Client.SocketStatusChange += Client_SocketStatusChange;
if(HeartbeatEnabled)
Client.SocketSendOrReceiveTimeOutInMs = (HeartbeatInterval * 5);
Client.AddressClientConnectedTo = Hostname;
Client.PortNumber = Port;
// SecureClient = c;
//var timeOfConnect = DateTime.Now.ToString("HH:mm:ss.fff");
ConnectFailTimer = new CTimer(o =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Connect attempt has not finished after 30sec Count:{0}", ConnectionCount);
if (IsTryingToConnect)
{
IsTryingToConnect = false;
//if (ConnectionHasHungCallback != null)
//{
// ConnectionHasHungCallback();
//}
//SecureClient.DisconnectFromServer();
//CheckClosedAndTryReconnect();
}
}, 30000);
Debug.Console(2, this, "Making Connection Count:{0}", ConnectionCount);
Client.ConnectToServerAsync(o =>
{
Debug.Console(2, this, "ConnectToServerAsync Count:{0} Ran!", ConnectionCount);
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
}
IsTryingToConnect = false;
if (o.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Debug.Console(2, this, "Client connected to {0} on port {1}", o.AddressClientConnectedTo, o.LocalPortNumberOfClient);
o.ReceiveDataAsync(Receive);
if (SharedKeyRequired)
{
WaitingForSharedKeyResponse = true;
WaitForSharedKey = new CTimer(timer =>
{
Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Shared key exchange timer expired. IsReadyForCommunication={0}", IsReadyForCommunication);
// Debug.Console(1, this, "Connect attempt failed {0}", c.ClientStatus);
// This is the only case where we should call DisconectFromServer...Event handeler will trigger the cleanup
o.DisconnectFromServer();
//CheckClosedAndTryReconnect();
//OnClientReadyForcommunications(false); // Should send false event
}, 15000);
}
else
{
//CLient connected and shared key is not required so just raise the ready for communication event. if Shared key
//required this is called by the shared key being negotiated
if (IsReadyForCommunication == false)
{
OnClientReadyForcommunications(true); // Key not required
}
}
}
else
{
Debug.Console(1, this, "Connect attempt failed {0}", o.ClientStatus);
CheckClosedAndTryReconnect();
}
});
}
catch (Exception ex)
{
Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Client connection exception: {0}", ex.Message);
IsTryingToConnect = false;
CheckClosedAndTryReconnect();
}
}
/// <summary>
///
/// </summary>
public void Disconnect()
{
Debug.Console(2, "Disconnect Called");
DisconnectCalledByUser = true;
if (IsConnected)
{
Client.DisconnectFromServer();
}
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
Cleanup();
}
/// <summary>
/// Internal call to close up client. ALWAYS use this when disconnecting.
/// </summary>
void Cleanup()
{
IsTryingToConnect = false;
if (Client != null)
{
//SecureClient.DisconnectFromServer();
Debug.Console(2, this, "Disconnecting Client {0}", DisconnectCalledByUser ? ", Called by user" : "");
Client.SocketStatusChange -= Client_SocketStatusChange;
Client.Dispose();
Client = null;
}
if (ConnectFailTimer != null)
{
ConnectFailTimer.Stop();
ConnectFailTimer.Dispose();
ConnectFailTimer = null;
}
}
/// <summary>ff
/// Called from Connect failure or Socket Status change if
/// auto reconnect and socket disconnected (Not disconnected by user)
/// </summary>
void CheckClosedAndTryReconnect()
{
if (Client != null)
{
Debug.Console(2, this, "Cleaning up remotely closed/failed connection.");
Cleanup();
}
if (!DisconnectCalledByUser && AutoReconnect)
{
var halfInterval = AutoReconnectIntervalMs / 2;
var rndTime = new Random().Next(-halfInterval, halfInterval) + AutoReconnectIntervalMs;
Debug.Console(2, this, "Attempting reconnect in {0} ms, randomized", rndTime);
if (RetryTimer != null)
{
RetryTimer.Stop();
RetryTimer = null;
}
RetryTimer = new CTimer(o => Connect(), rndTime);
}
}
/// <summary>
/// Receive callback
/// </summary>
/// <param name="client"></param>
/// <param name="numBytes"></param>
void Receive(TCPClient client, int numBytes)
{
if (numBytes > 0)
{
string str = string.Empty;
try
{
var bytes = client.IncomingDataBuffer.Take(numBytes).ToArray();
str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
Debug.Console(2, this, "Client Received:\r--------\r{0}\r--------", str);
if (!string.IsNullOrEmpty(checkHeartbeat(str)))
{
if (SharedKeyRequired && str == "SharedKey:")
{
Debug.Console(2, this, "Server asking for shared key, sending");
SendText(SharedKey + "\n");
}
else if (SharedKeyRequired && str == "Shared Key Match")
{
StopWaitForSharedKeyTimer();
Debug.Console(2, this, "Shared key confirmed. Ready for communication");
OnClientReadyForcommunications(true); // Successful key exchange
}
else
{
//var bytesHandler = BytesReceived;
//if (bytesHandler != null)
// bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
var textHandler = TextReceived;
if (textHandler != null)
textHandler(this, new GenericTcpServerCommMethodReceiveTextArgs(str));
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error receiving data: {1}. Error: {0}", ex.Message, str);
}
}
if (client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
client.ReceiveDataAsync(Receive);
}
void HeartbeatStart()
{
if (HeartbeatEnabled)
{
Debug.Console(2, this, "Starting Heartbeat");
if (HeartbeatSendTimer == null)
{
HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval);
}
if (HeartbeatAckTimer == null)
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
}
}
void HeartbeatStop()
{
if (HeartbeatSendTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Send");
HeartbeatSendTimer.Stop();
HeartbeatSendTimer = null;
}
if (HeartbeatAckTimer != null)
{
Debug.Console(2, this, "Stoping Heartbeat Ack");
HeartbeatAckTimer.Stop();
HeartbeatAckTimer = null;
}
}
void SendHeartbeat(object notused)
{
this.SendText(HeartbeatString);
Debug.Console(2, this, "Sending Heartbeat");
}
//private method to check heartbeat requirements and start or reset timer
string checkHeartbeat(string received)
{
try
{
if (HeartbeatEnabled)
{
if (!string.IsNullOrEmpty(HeartbeatString))
{
var remainingText = received.Replace(HeartbeatString, "");
var noDelimiter = received.Trim(new char[] { '\r', '\n' });
if (noDelimiter.Contains(HeartbeatString))
{
if (HeartbeatAckTimer != null)
{
HeartbeatAckTimer.Reset(HeartbeatInterval * 2);
}
else
{
HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2));
}
Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString);
return remainingText;
}
}
}
}
catch (Exception ex)
{
Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
}
return received;
}
void HeartbeatAckTimerFail(object o)
{
try
{
if (IsConnected)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE");
SendText("Heartbeat not received by server, closing connection");
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex);
}
}
/// <summary>
///
/// </summary>
void StopWaitForSharedKeyTimer()
{
if (WaitForSharedKey != null)
{
WaitForSharedKey.Stop();
WaitForSharedKey = null;
}
}
/// <summary>
/// General send method
/// </summary>
public void SendText(string text)
{
if (!string.IsNullOrEmpty(text))
{
try
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
{
Client.SendDataAsync(bytes, bytes.Length, (c, n) =>
{
// HOW IN THE HELL DO WE CATCH AN EXCEPTION IN SENDING?????
if (n <= 0)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "[{0}] Sent zero bytes. Was there an error?", this.Key);
}
});
}
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending text: {1}. Error: {0}", ex.Message, text);
}
}
}
/// <summary>
///
/// </summary>
public void SendBytes(byte[] bytes)
{
if (bytes.Length > 0)
{
try
{
if (Client != null && Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED)
Client.SendData(bytes, bytes.Length);
}
catch (Exception ex)
{
Debug.Console(0, this, "Error sending bytes. Error: {0}", ex.Message);
}
}
}
/// <summary>
/// SocketStatusChange Callback
/// </summary>
/// <param name="client"></param>
/// <param name="clientSocketStatus"></param>
void Client_SocketStatusChange(TCPClient client, SocketStatus clientSocketStatus)
{
if (ProgramIsStopping)
{
ProgramIsStopping = false;
return;
}
try
{
Debug.Console(2, this, "Socket status change: {0} ({1})", client.ClientStatus, (ushort)(client.ClientStatus));
OnConnectionChange();
// The client could be null or disposed by this time...
if (Client == null || Client.ClientStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
{
HeartbeatStop();
OnClientReadyForcommunications(false); // socket has gone low
CheckClosedAndTryReconnect();
}
}
catch (Exception ex)
{
Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Error in socket status change callback. Error: {0}\r\r{1}", ex, ex.InnerException);
}
}
/// <summary>
/// Helper for ConnectionChange event
/// </summary>
void OnConnectionChange()
{
var handler = ConnectionChange;
if (handler != null)
ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
}
/// <summary>
/// Helper to fire ClientReadyForCommunications event
/// </summary>
void OnClientReadyForcommunications(bool isReady)
{
IsReadyForCommunication = isReady;
if (this.IsReadyForCommunication) { HeartbeatStart(); }
var handler = ClientReadyForCommunications;
if (handler != null)
handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
}
#endregion
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,395 @@
using System;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// Generic UDP Server device
/// </summary>
public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
{
private const string SplusKey = "Uninitialized Udp Server";
/// <summary>
/// Object to enable stream debugging
/// </summary>
public CommunicationStreamDebugging StreamDebugging { get; private set; }
/// <summary>
///
/// </summary>
public event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
///
/// </summary>
public event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// This event will fire when a message is dequeued that includes the source IP and Port info if needed to determine the source of the received data.
/// </summary>
public event EventHandler<GenericUdpReceiveTextExtraArgs> DataRecievedExtra;
/// <summary>
///
/// </summary>
public event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
///
/// </summary>
public event EventHandler<GenericUdpConnectedEventArgs> UpdateConnectionStatus;
/// <summary>
///
/// </summary>
public SocketStatus ClientStatus
{
get
{
return Server.ServerStatus;
}
}
/// <summary>
///
/// </summary>
public ushort UStatus
{
get { return (ushort)Server.ServerStatus; }
}
/// <summary>
/// Address of server
/// </summary>
public string Hostname { get; set; }
/// <summary>
/// Port on server
/// </summary>
public int Port { get; set; }
/// <summary>
/// Another damn S+ helper because S+ seems to treat large port nums as signed ints
/// which screws up things
/// </summary>
public ushort UPort
{
get { return Convert.ToUInt16(Port); }
set { Port = Convert.ToInt32(value); }
}
/// <summary>
/// Indicates that the UDP Server is enabled
/// </summary>
public bool IsConnected
{
get;
private set;
}
/// <summary>
/// Numeric value indicating
/// </summary>
public ushort UIsConnected
{
get { return IsConnected ? (ushort)1 : (ushort)0; }
}
/// <summary>
/// Defaults to 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// The server
/// </summary>
public UDPServer Server { get; private set; }
/// <summary>
/// Constructor for S+. Make sure to set key, address, port, and buffersize using init method
/// </summary>
public GenericUdpServer()
: base(SplusKey)
{
StreamDebugging = new CommunicationStreamDebugging(SplusKey);
BufferSize = 5000;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
/// <param name="buffefSize"></param>
public GenericUdpServer(string key, string address, int port, int buffefSize)
: base(key)
{
StreamDebugging = new CommunicationStreamDebugging(key);
Hostname = address;
Port = port;
BufferSize = buffefSize;
CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
}
/// <summary>
/// Call from S+ to initialize values
/// </summary>
/// <param name="key"></param>
/// <param name="address"></param>
/// <param name="port"></param>
public void Initialize(string key, string address, ushort port)
{
Key = key;
Hostname = address;
UPort = port;
}
/// <summary>
///
/// </summary>
/// <param name="ethernetEventArgs"></param>
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
{
// Re-enable the server if the link comes back up and the status should be connected
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp
&& IsConnected)
{
Connect();
}
}
/// <summary>
///
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType != eProgramStatusEventType.Stopping)
return;
Debug.Console(1, this, "Program stopping. Disabling Server");
Disconnect();
}
/// <summary>
/// Enables the UDP Server
/// </summary>
public void Connect()
{
if (Server == null)
{
Server = new UDPServer();
}
if (string.IsNullOrEmpty(Hostname))
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': No address set", Key);
return;
}
if (Port < 1 || Port > 65535)
{
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "GenericUdpServer '{0}': Invalid port", Key);
return;
}
}
var status = Server.EnableUDPServer(Hostname, Port);
Debug.Console(2, this, "SocketErrorCode: {0}", status);
if (status == SocketErrorCodes.SOCKET_OK)
IsConnected = true;
var handler = UpdateConnectionStatus;
if (handler != null)
handler(this, new GenericUdpConnectedEventArgs(UIsConnected));
// Start receiving data
Server.ReceiveDataAsync(Receive);
}
/// <summary>
/// Disabled the UDP Server
/// </summary>
public void Disconnect()
{
if(Server != null)
Server.DisableUDPServer();
IsConnected = false;
var handler = UpdateConnectionStatus;
if (handler != null)
handler(this, new GenericUdpConnectedEventArgs(UIsConnected));
}
/// <summary>
/// Recursive method to receive data
/// </summary>
/// <param name="server"></param>
/// <param name="numBytes"></param>
void Receive(UDPServer server, int numBytes)
{
Debug.Console(2, this, "Received {0} bytes", numBytes);
try
{
if (numBytes <= 0)
return;
var sourceIp = Server.IPAddressLastMessageReceivedFrom;
var sourcePort = Server.IPPortLastMessageReceivedFrom;
var bytes = server.IncomingDataBuffer.Take(numBytes).ToArray();
var str = Encoding.GetEncoding(28591).GetString(bytes, 0, bytes.Length);
var dataRecivedExtra = DataRecievedExtra;
if (dataRecivedExtra != null)
dataRecivedExtra(this, new GenericUdpReceiveTextExtraArgs(str, sourceIp, sourcePort, bytes));
Debug.Console(2, this, "Bytes: {0}", bytes.ToString());
var bytesHandler = BytesReceived;
if (bytesHandler != null)
{
if (StreamDebugging.RxStreamDebuggingIsEnabled)
{
Debug.Console(0, this, "Received {1} bytes: '{0}'", ComTextHelper.GetEscapedText(bytes), bytes.Length);
}
bytesHandler(this, new GenericCommMethodReceiveBytesArgs(bytes));
}
var textHandler = TextReceived;
if (textHandler != null)
{
if (StreamDebugging.RxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Received {1} characters of text: '{0}'", ComTextHelper.GetDebugText(str), str.Length);
textHandler(this, new GenericCommMethodReceiveTextArgs(str));
}
}
catch (Exception ex)
{
Debug.Console(0, "GenericUdpServer Receive error: {0}{1}", ex.Message, ex.StackTrace);
}
finally
{
server.ReceiveDataAsync(Receive);
}
}
/// <summary>
/// General send method
/// </summary>
/// <param name="text"></param>
public void SendText(string text)
{
var bytes = Encoding.GetEncoding(28591).GetBytes(text);
if (IsConnected && Server != null)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} characters of text: '{1}'", text.Length, ComTextHelper.GetDebugText(text));
Server.SendData(bytes, bytes.Length);
}
}
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
public void SendBytes(byte[] bytes)
{
if (StreamDebugging.TxStreamDebuggingIsEnabled)
Debug.Console(0, this, "Sending {0} bytes: '{1}'", bytes.Length, ComTextHelper.GetEscapedText(bytes));
if (IsConnected && Server != null)
Server.SendData(bytes, bytes.Length);
}
}
/// <summary>
///
/// </summary>
public class GenericUdpReceiveTextExtraArgs : EventArgs
{
/// <summary>
///
/// </summary>
public string Text { get; private set; }
/// <summary>
///
/// </summary>
public string IpAddress { get; private set; }
/// <summary>
///
/// </summary>
public int Port { get; private set; }
/// <summary>
///
/// </summary>
public byte[] Bytes { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="ipAddress"></param>
/// <param name="port"></param>
/// <param name="bytes"></param>
public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes)
{
Text = text;
IpAddress = ipAddress;
Port = port;
Bytes = bytes;
}
/// <summary>
/// Stupid S+ Constructor
/// </summary>
public GenericUdpReceiveTextExtraArgs() { }
}
/// <summary>
///
/// </summary>
public class UdpServerPropertiesConfig
{
/// <summary>
///
/// </summary>
[JsonProperty(Required = Required.Always)]
public string Address { get; set; }
/// <summary>
///
/// </summary>
[JsonProperty(Required = Required.Always)]
public int Port { get; set; }
/// <summary>
/// Defaults to 32768
/// </summary>
public int BufferSize { get; set; }
/// <summary>
///
/// </summary>
public UdpServerPropertiesConfig()
{
BufferSize = 32768;
}
}
}

View File

@@ -0,0 +1,59 @@
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// Client config object for TCP client with server that inherits from TcpSshPropertiesConfig and adds properties for shared key and heartbeat
/// </summary>
public class TcpClientConfigObject
{
/// <summary>
/// TcpSsh Properties
/// </summary>
[JsonProperty("control")]
public ControlPropertiesConfig Control { get; set; }
/// <summary>
/// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic
/// </summary>
[JsonProperty("secure")]
public bool Secure { get; set; }
/// <summary>
/// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client
/// </summary>
[JsonProperty("sharedKeyRequired")]
public bool SharedKeyRequired { get; set; }
/// <summary>
/// The shared key that must match on the server and client
/// </summary>
[JsonProperty("sharedKey")]
public string SharedKey { get; set; }
/// <summary>
/// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received.
/// heartbeats do not raise received events.
/// </summary>
[JsonProperty("heartbeatRequired")]
public bool HeartbeatRequired { get; set; }
/// <summary>
/// The interval in seconds for the heartbeat from the client. If not received client is disconnected
/// </summary>
[JsonProperty("heartbeatRequiredIntervalInSeconds")]
public ushort HeartbeatRequiredIntervalInSeconds { get; set; }
/// <summary>
/// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided.
/// </summary>
[JsonProperty("heartbeatStringToMatch")]
public string HeartbeatStringToMatch { get; set; }
/// <summary>
/// Receive Queue size must be greater than 20 or defaults to 20
/// </summary>
[JsonProperty("receiveQueueSize")]
public int ReceiveQueueSize { get; set; }
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities
/// </summary>
public class TcpServerConfigObject
{
/// <summary>
/// Uique key
/// </summary>
public string Key { get; set; }
/// <summary>
/// Max Clients that the server will allow to connect.
/// </summary>
public ushort MaxClients { get; set; }
/// <summary>
/// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic
/// </summary>
public bool Secure { get; set; }
/// <summary>
/// Port for the server to listen on
/// </summary>
public int Port { get; set; }
/// <summary>
/// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client
/// </summary>
public bool SharedKeyRequired { get; set; }
/// <summary>
/// The shared key that must match on the server and client
/// </summary>
public string SharedKey { get; set; }
/// <summary>
/// Require a heartbeat on the client/server connection that will cause the server/client to disconnect if the heartbeat is not received.
/// heartbeats do not raise received events.
/// </summary>
public bool HeartbeatRequired { get; set; }
/// <summary>
/// The interval in seconds for the heartbeat from the client. If not received client is disconnected
/// </summary>
public ushort HeartbeatRequiredIntervalInSeconds { get; set; }
/// <summary>
/// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided.
/// </summary>
public string HeartbeatStringToMatch { get; set; }
/// <summary>
/// Client buffer size. See Crestron help. defaults to 2000 if not greater than 2000
/// </summary>
public int BufferSize { get; set; }
/// <summary>
/// Receive Queue size must be greater than 20 or defaults to 20
/// </summary>
public int ReceiveQueueSize { get; set; }
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Crestron Control Methods for a comm object
/// </summary>
public enum eControlMethod
{
/// <summary>
///
/// </summary>
None = 0,
/// <summary>
/// RS232/422/485
/// </summary>
Com,
/// <summary>
/// Crestron IpId (most Crestron ethernet devices)
/// </summary>
IpId,
/// <summary>
/// Crestron IpIdTcp (HD-MD series, etc.)
/// </summary>
IpidTcp,
/// <summary>
/// Crestron IR control
/// </summary>
IR,
/// <summary>
/// SSH client
/// </summary>
Ssh,
/// <summary>
/// TCP/IP client
/// </summary>
Tcpip,
/// <summary>
/// Telnet
/// </summary>
Telnet,
/// <summary>
/// Crestnet device
/// </summary>
Cresnet,
/// <summary>
/// CEC Control, via a DM HDMI port
/// </summary>
Cec,
/// <summary>
/// UDP Server
/// </summary>
Udp,
/// <summary>
/// HTTP client
/// </summary>
Http,
/// <summary>
/// HTTPS client
/// </summary>
Https,
/// <summary>
/// Websocket client
/// </summary>
Ws,
/// <summary>
/// Secure Websocket client
/// </summary>
Wss,
/// <summary>
/// Secure TCP/IP
/// </summary>
SecureTcpIp
}
}

View File

@@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronSockets;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// An incoming communication stream
/// </summary>
public interface ICommunicationReceiver : IKeyed
{
/// <summary>
/// Notifies of bytes received
/// </summary>
event EventHandler<GenericCommMethodReceiveBytesArgs> BytesReceived;
/// <summary>
/// Notifies of text received
/// </summary>
event EventHandler<GenericCommMethodReceiveTextArgs> TextReceived;
/// <summary>
/// Indicates connection status
/// </summary>
[JsonProperty("isConnected")]
bool IsConnected { get; }
/// <summary>
/// Connect to the device
/// </summary>
void Connect();
/// <summary>
/// Disconnect from the device
/// </summary>
void Disconnect();
}
/// <summary>
/// Represents a device that uses basic connection
/// </summary>
public interface IBasicCommunication : ICommunicationReceiver
{
/// <summary>
/// Send text to the device
/// </summary>
/// <param name="text"></param>
void SendText(string text);
/// <summary>
/// Send bytes to the device
/// </summary>
/// <param name="bytes"></param>
void SendBytes(byte[] bytes);
}
/// <summary>
/// Represents a device that implements IBasicCommunication and IStreamDebugging
/// </summary>
public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, IStreamDebugging
{
}
/// <summary>
/// Represents a device with stream debugging capablities
/// </summary>
public interface IStreamDebugging
{
/// <summary>
/// Object to enable stream debugging
/// </summary>
[JsonProperty("streamDebugging")]
CommunicationStreamDebugging StreamDebugging { get; }
}
/// <summary>
/// For IBasicCommunication classes that have SocketStatus. GenericSshClient,
/// GenericTcpIpClient
/// </summary>
public interface ISocketStatus : IBasicCommunication
{
/// <summary>
/// Notifies of socket status changes
/// </summary>
event EventHandler<GenericSocketStatusChageEventArgs> ConnectionChange;
/// <summary>
/// The current socket status of the client
/// </summary>
[JsonProperty("clientStatus")]
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
SocketStatus ClientStatus { get; }
}
/// <summary>
/// Describes a device that implements ISocketStatus and IStreamDebugging
/// </summary>
public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugging
{
}
/// <summary>
/// Describes a device that can automatically attempt to reconnect
/// </summary>
public interface IAutoReconnect
{
/// <summary>
/// Enable automatic recconnect
/// </summary>
[JsonProperty("autoReconnect")]
bool AutoReconnect { get; set; }
/// <summary>
/// Interval in ms to attempt automatic recconnections
/// </summary>
[JsonProperty("autoReconnectIntervalMs")]
int AutoReconnectIntervalMs { get; set; }
}
/// <summary>
///
/// </summary>
public enum eGenericCommMethodStatusChangeType
{
/// <summary>
/// Connected
/// </summary>
Connected,
/// <summary>
/// Disconnected
/// </summary>
Disconnected
}
/// <summary>
/// This delegate defines handler for IBasicCommunication status changes
/// </summary>
/// <param name="comm">Device firing the status change</param>
/// <param name="status"></param>
public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status);
/// <summary>
///
/// </summary>
public class GenericCommMethodReceiveBytesArgs : EventArgs
{
/// <summary>
///
/// </summary>
public byte[] Bytes { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
public GenericCommMethodReceiveBytesArgs(byte[] bytes)
{
Bytes = bytes;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericCommMethodReceiveBytesArgs() { }
}
/// <summary>
///
/// </summary>
public class GenericCommMethodReceiveTextArgs : EventArgs
{
/// <summary>
///
/// </summary>
public string Text { get; private set; }
/// <summary>
///
/// </summary>
public string Delimiter { get; private set; }
/// <summary>
///
/// </summary>
/// <param name="text"></param>
public GenericCommMethodReceiveTextArgs(string text)
{
Text = text;
}
/// <summary>
///
/// </summary>
/// <param name="text"></param>
/// <param name="delimiter"></param>
public GenericCommMethodReceiveTextArgs(string text, string delimiter)
:this(text)
{
Delimiter = delimiter;
}
/// <summary>
/// S+ Constructor
/// </summary>
public GenericCommMethodReceiveTextArgs() { }
}
/// <summary>
///
/// </summary>
public class ComTextHelper
{
/// <summary>
/// Gets escaped text for a byte array
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string GetEscapedText(byte[] bytes)
{
return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
}
/// <summary>
/// Gets escaped text for a string
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
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>
public static string GetDebugText(string text)
{
return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value));
}
}
}

View File

@@ -0,0 +1,235 @@
using System;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core;
using Serilog.Events;
namespace PepperDash.Core.Config
{
/// <summary>
/// Reads a Portal formatted config file
/// </summary>
public class PortalConfigReader
{
/// <summary>
/// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object.
/// </summary>
/// <returns>JObject of config file</returns>
public static void ReadAndMergeFileIfNecessary(string filePath, string savePath)
{
try
{
if (!File.Exists(filePath))
{
Debug.Console(1, Debug.ErrorLogLevel.Error,
"ERROR: Configuration file not present. Please load file to {0} and reset program", filePath);
}
using (StreamReader fs = new StreamReader(filePath))
{
var jsonObj = JObject.Parse(fs.ReadToEnd());
if(jsonObj["template"] != null && jsonObj["system"] != null)
{
// it's a double-config, merge it.
var merged = MergeConfigs(jsonObj);
if (jsonObj["system_url"] != null)
{
merged["systemUrl"] = jsonObj["system_url"].Value<string>();
}
if (jsonObj["template_url"] != null)
{
merged["templateUrl"] = jsonObj["template_url"].Value<string>();
}
jsonObj = merged;
}
using (StreamWriter fw = new StreamWriter(savePath))
{
fw.Write(jsonObj.ToString(Formatting.Indented));
Debug.LogMessage(LogEventLevel.Debug, "JSON config merged and saved to {0}", savePath);
}
}
}
catch (Exception e)
{
Debug.LogMessage(e, "ERROR: Config load failed");
}
}
/// <summary>
///
/// </summary>
/// <param name="doubleConfig"></param>
/// <returns></returns>
public static JObject MergeConfigs(JObject doubleConfig)
{
var system = JObject.FromObject(doubleConfig["system"]);
var template = JObject.FromObject(doubleConfig["template"]);
var merged = new JObject();
// Put together top-level objects
if (system["info"] != null)
merged.Add("info", Merge(template["info"], system["info"], "infO"));
else
merged.Add("info", template["info"]);
merged.Add("devices", MergeArraysOnTopLevelProperty(template["devices"] as JArray,
system["devices"] as JArray, "key", "devices"));
if (system["rooms"] == null)
merged.Add("rooms", template["rooms"]);
else
merged.Add("rooms", MergeArraysOnTopLevelProperty(template["rooms"] as JArray,
system["rooms"] as JArray, "key", "rooms"));
if (system["sourceLists"] == null)
merged.Add("sourceLists", template["sourceLists"]);
else
merged.Add("sourceLists", Merge(template["sourceLists"], system["sourceLists"], "sourceLists"));
if (system["destinationLists"] == null)
merged.Add("destinationLists", template["destinationLists"]);
else
merged.Add("destinationLists",
Merge(template["destinationLists"], system["destinationLists"], "destinationLists"));
if (system["cameraLists"] == null)
merged.Add("cameraLists", template["cameraLists"]);
else
merged.Add("cameraLists", Merge(template["cameraLists"], system["cameraLists"], "cameraLists"));
if (system["audioControlPointLists"] == null)
merged.Add("audioControlPointLists", template["audioControlPointLists"]);
else
merged.Add("audioControlPointLists",
Merge(template["audioControlPointLists"], system["audioControlPointLists"], "audioControlPointLists"));
// Template tie lines take precedence. Config tool doesn't do them at system
// level anyway...
if (template["tieLines"] != null)
merged.Add("tieLines", template["tieLines"]);
else if (system["tieLines"] != null)
merged.Add("tieLines", system["tieLines"]);
else
merged.Add("tieLines", new JArray());
if (template["joinMaps"] != null)
merged.Add("joinMaps", template["joinMaps"]);
else
merged.Add("joinMaps", new JObject());
if (system["global"] != null)
merged.Add("global", Merge(template["global"], system["global"], "global"));
else
merged.Add("global", template["global"]);
//Debug.Console(2, "MERGED CONFIG RESULT: \x0d\x0a{0}", merged);
return merged;
}
/// <summary>
/// Merges the contents of a base and a delta array, matching the entries on a top-level property
/// given by propertyName. Returns a merge of them. Items in the delta array that do not have
/// a matched item in base array will not be merged. Non keyed system items will replace the template items.
/// </summary>
static JArray MergeArraysOnTopLevelProperty(JArray a1, JArray a2, string propertyName, string path)
{
var result = new JArray();
if (a2 == null || a2.Count == 0) // If the system array is null or empty, return the template array
return a1;
else if (a1 != null)
{
if (a2[0]["key"] == null) // If the first item in the system array has no key, overwrite the template array
{ // with the system array
return a2;
}
else // The arrays are keyed, merge them by key
{
for (int i = 0; i < a1.Count(); i++)
{
var a1Dev = a1[i];
// Try to get a system device and if found, merge it onto template
var a2Match = a2.FirstOrDefault(t => t[propertyName].Equals(a1Dev[propertyName]));// t.Value<int>("uid") == tmplDev.Value<int>("uid"));
if (a2Match != null)
{
var mergedItem = Merge(a1Dev, a2Match, string.Format("{0}[{1}].", path, i));// Merge(JObject.FromObject(a1Dev), JObject.FromObject(a2Match));
result.Add(mergedItem);
}
else
result.Add(a1Dev);
}
}
}
return result;
}
/// <summary>
/// Helper for using with JTokens. Converts to JObject
/// </summary>
static JObject Merge(JToken t1, JToken t2, string path)
{
return Merge(JObject.FromObject(t1), JObject.FromObject(t2), path);
}
/// <summary>
/// Merge o2 onto o1
/// </summary>
/// <param name="o1"></param>
/// <param name="o2"></param>
/// <param name="path"></param>
static JObject Merge(JObject o1, JObject o2, string path)
{
foreach (var o2Prop in o2)
{
var propKey = o2Prop.Key;
var o1Value = o1[propKey];
var o2Value = o2[propKey];
// if the property doesn't exist on o1, then add it.
if (o1Value == null)
{
o1.Add(propKey, o2Value);
}
// otherwise merge them
else
{
// Drill down
var propPath = String.Format("{0}.{1}", path, propKey);
try
{
if (o1Value is JArray)
{
if (o2Value is JArray)
{
o1Value.Replace(MergeArraysOnTopLevelProperty(o1Value as JArray, o2Value as JArray, "key", propPath));
}
}
else if (o2Prop.Value.HasValues && o1Value.HasValues)
{
o1Value.Replace(Merge(JObject.FromObject(o1Value), JObject.FromObject(o2Value), propPath));
}
else
{
o1Value.Replace(o2Prop.Value);
}
}
catch (Exception e)
{
Debug.Console(1, Debug.ErrorLogLevel.Warning, "Cannot merge items at path {0}: \r{1}", propPath, e);
}
}
}
return o1;
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
public class EncodingHelper
{
public static string ConvertUtf8ToAscii(string utf8String)
{
return Encoding.ASCII.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length);
}
public static string ConvertUtf8ToUtf16(string utf8String)
{
return Encoding.Unicode.GetString(Encoding.UTF8.GetBytes(utf8String), 0, utf8String.Length);
}
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Newtonsoft.Json;
using Serilog;
namespace PepperDash.Core
{
/// <summary>
/// Unique key interface to require a unique key for the class
/// </summary>
public interface IKeyed
{
/// <summary>
/// Unique Key
/// </summary>
[JsonProperty("key")]
string Key { get; }
}
/// <summary>
/// Named Keyed device interface. Forces the device to have a Unique Key and a name.
/// </summary>
public interface IKeyName : IKeyed
{
/// <summary>
/// Isn't it obvious :)
/// </summary>
[JsonProperty("name")]
string Name { get; }
}
}

View File

@@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using Serilog.Events;
namespace PepperDash.Core
{
//*********************************************************************************************************
/// <summary>
/// The core event and status-bearing class that most if not all device and connectors can derive from.
/// </summary>
public class Device : IKeyName
{
/// <summary>
/// Unique Key
/// </summary>
public string Key { get; protected set; }
/// <summary>
/// Name of the devie
/// </summary>
public string Name { get; protected set; }
/// <summary>
///
/// </summary>
public bool Enabled { get; protected set; }
///// <summary>
///// A place to store reference to the original config object, if any. These values should
///// NOT be used as properties on the device as they are all publicly-settable values.
///// </summary>
//public DeviceConfig Config { get; private set; }
///// <summary>
///// Helper method to check if Config exists
///// </summary>
//public bool HasConfig { get { return Config != null; } }
List<Action> _PreActivationActions;
List<Action> _PostActivationActions;
/// <summary>
///
/// </summary>
public static Device DefaultDevice { get { return _DefaultDevice; } }
static Device _DefaultDevice = new Device("Default", "Default");
/// <summary>
/// Base constructor for all Devices.
/// </summary>
/// <param name="key"></param>
public Device(string key)
{
Key = key;
if (key.Contains(".")) Debug.LogMessage(LogEventLevel.Information, "WARNING: Device key should not include '.'", this);
Name = "";
}
/// <summary>
/// Constructor with key and name
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
public Device(string key, string name) : this(key)
{
Name = name;
}
//public Device(DeviceConfig config)
// : this(config.Key, config.Name)
//{
// Config = config;
//}
/// <summary>
/// Adds a pre activation action
/// </summary>
/// <param name="act"></param>
public void AddPreActivationAction(Action act)
{
if (_PreActivationActions == null)
_PreActivationActions = new List<Action>();
_PreActivationActions.Add(act);
}
/// <summary>
/// Adds a post activation action
/// </summary>
/// <param name="act"></param>
public void AddPostActivationAction(Action act)
{
if (_PostActivationActions == null)
_PostActivationActions = new List<Action>();
_PostActivationActions.Add(act);
}
/// <summary>
/// Executes the preactivation actions
/// </summary>
public void PreActivate()
{
if (_PreActivationActions != null)
_PreActivationActions.ForEach(a => {
try
{
a.Invoke();
} catch (Exception e)
{
Debug.LogMessage(e, "Error in PreActivationAction: " + e.Message, this);
}
});
}
/// <summary>
/// Gets this device ready to be used in the system. Runs any added pre-activation items, and
/// all post-activation at end. Classes needing additional logic to
/// run should override CustomActivate()
/// </summary>
public bool Activate()
{
//if (_PreActivationActions != null)
// _PreActivationActions.ForEach(a => a.Invoke());
var result = CustomActivate();
//if(result && _PostActivationActions != null)
// _PostActivationActions.ForEach(a => a.Invoke());
return result;
}
/// <summary>
/// Executes the postactivation actions
/// </summary>
public void PostActivate()
{
if (_PostActivationActions != null)
_PostActivationActions.ForEach(a => {
try
{
a.Invoke();
}
catch (Exception e)
{
Debug.LogMessage(e, "Error in PostActivationAction: " + e.Message, this);
}
});
}
/// <summary>
/// Called in between Pre and PostActivationActions when Activate() is called.
/// Override to provide addtitional setup when calling activation. Overriding classes
/// do not need to call base.CustomActivate()
/// </summary>
/// <returns>true if device activated successfully.</returns>
public virtual bool CustomActivate() { return true; }
/// <summary>
/// Call to deactivate device - unlink events, etc. Overriding classes do not
/// need to call base.Deactivate()
/// </summary>
/// <returns></returns>
public virtual bool Deactivate() { return true; }
/// <summary>
/// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
/// </summary>
public virtual void Initialize()
{
}
/// <summary>
/// Helper method to check object for bool value false and fire an Action method
/// </summary>
/// <param name="o">Should be of type bool, others will be ignored</param>
/// <param name="a">Action to be run when o is false</param>
public void OnFalse(object o, Action a)
{
if (o is bool && !(bool)o) a();
}
}
}

View File

@@ -0,0 +1,43 @@
<Project>
<ItemGroup>
<None Include="$(TargetDir)\$(TargetName).$(Version).cpz" Condition="$(ProjectType) == 'Program'">
<Pack>true</Pack>
<PackagePath>content;</PackagePath>
</None>
<None Include="$(PackageOutputPath)\$(TargetName).$(Version).cplz" Condition="$(ProjectType) == 'ProgramLibrary'">
<Pack>true</Pack>
<PackagePath>content;</PackagePath>
</None>
</ItemGroup>
<Target Name="Create CPLZ" AfterTargets="Build; Rebuild" Condition="$(ProjectType) == 'ProgramLibrary'">
<Message Text="Creating CPLZ"></Message>
<MakeDir Directories="$(PackageOutputPath)" Condition="!Exists($(PackageOutputPath))"></MakeDir>
<ZipDirectory SourceDirectory="$(TargetDir)" DestinationFile="$(PackageOutputPath)\$(TargetName).$(Version).cplz" Overwrite="true"/>
</Target>
<Target Name="Clean CPLZ" AfterTargets="AfterClean" Condition="$(ProjectType) == 'ProgramLibrary'">
<Delete Files="$(PackageOutputPath)\$(TargetName).$(Version).cplz"/>
</Target>
<Target Name="Copy CPZ" AfterTargets="SimplSharpPostProcess" Condition="$(ProjectType) == 'Program'">
<Message Text="Copying CPZ"></Message>
<Move SourceFiles="$(TargetDir)\$(TargetName).cpz" DestinationFiles="$(TargetDir)\$(TargetName).$(Version).cpz"/>
<Copy SourceFiles="$(TargetDir)\$(TargetName).$(Version).cpz" DestinationFiles="$(PackageOutputPath)\$(TargetName).$(Version).cpz"/>
</Target>
<Target Name="Clean CPZ" AfterTargets="AfterClean" Condition="$(ProjectType) == 'Program'">
<Delete Files="$(PackageOutputPath)\$(TargetName).$(Version).cpz"/>
</Target>
<Target Name="Copy CLZ" AfterTargets="SimplSharpPostProcess">
<Message Text="Copying CLZ"></Message>
<Move SourceFiles="$(TargetDir)\$(TargetName).clz" DestinationFiles="$(TargetDir)\$(TargetName).$(Version).clz"/>
<Copy SourceFiles="$(TargetDir)\$(TargetName).$(Version).clz" DestinationFiles="$(PackageOutputPath)\$(TargetName).$(Version).clz"/>
</Target>
<Target Name="Clean CLZ" AfterTargets="AfterClean">
<Delete Files="$(PackageOutputPath)\$(TargetName).$(Version).clz"/>
</Target>
<Target Name="SimplSharpNewtonsoft" BeforeTargets="FindReferenceAssembliesForReferences;ResolveReferences">
<ItemGroup>
<ReferencePath Condition="'%(FileName)' == 'Newtonsoft.Json.Compact'">
<Aliases>doNotUse</Aliases>
</ReferencePath>
</ItemGroup>
</Target>
</Project>

View File

@@ -0,0 +1,21 @@
name: Build Essentials Plugin
on:
push:
branches:
- '**'
jobs:
getVersion:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-getversion.yml@main
secrets: inherit
build-4Series:
uses: PepperDash/workflow-templates/.github/workflows/essentialsplugins-4Series-builds.yml@main
secrets: inherit
needs: getVersion
if: needs.getVersion.outputs.newVersion == 'true'
with:
newVersion: ${{ needs.getVersion.outputs.newVersion }}
version: ${{ needs.getVersion.outputs.version }}
tag: ${{ needs.getVersion.outputs.tag }}
channel: ${{ needs.getVersion.outputs.channel }}

View File

@@ -0,0 +1,116 @@
using Crestron.SimplSharp;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// Class to help with accessing values from the CrestronEthernetHelper class
/// </summary>
public class EthernetHelper
{
/// <summary>
///
/// </summary>
public static EthernetHelper LanHelper
{
get
{
if (_LanHelper == null) _LanHelper = new EthernetHelper(0);
return _LanHelper;
}
}
static EthernetHelper _LanHelper;
// ADD OTHER HELPERS HERE
/// <summary>
///
/// </summary>
public int PortNumber { get; private set; }
private EthernetHelper(int portNumber)
{
PortNumber = portNumber;
}
/// <summary>
///
/// </summary>
[JsonProperty("linkActive")]
public bool LinkActive
{
get
{
var status = CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_LINK_STATUS, 0);
Debug.Console(0, "LinkActive = {0}", status);
return status == "";
}
}
/// <summary>
///
/// </summary>
[JsonProperty("dchpActive")]
public bool DhcpActive
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, 0) == "ON";
}
}
/// <summary>
///
/// </summary>
[JsonProperty("hostname")]
public string Hostname
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
}
}
/// <summary>
///
/// </summary>
[JsonProperty("ipAddress")]
public string IPAddress
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
}
}
/// <summary>
///
/// </summary>
[JsonProperty("subnetMask")]
public string SubnetMask
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, 0);
}
}
/// <summary>
///
/// </summary>
[JsonProperty("defaultGateway")]
public string DefaultGateway
{
get
{
return CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, 0);
}
}
}
}

View File

@@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Bool change event args
/// </summary>
public class BoolChangeEventArgs : EventArgs
{
/// <summary>
/// Boolean state property
/// </summary>
public bool State { get; set; }
/// <summary>
/// Boolean ushort value property
/// </summary>
public ushort IntValue { get { return (ushort)(State ? 1 : 0); } }
/// <summary>
/// Boolean change event args type
/// </summary>
public ushort Type { get; set; }
/// <summary>
/// Boolean change event args index
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public BoolChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="state"></param>
/// <param name="type"></param>
public BoolChangeEventArgs(bool state, ushort type)
{
State = state;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="state"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public BoolChangeEventArgs(bool state, ushort type, ushort index)
{
State = state;
Type = type;
Index = index;
}
}
/// <summary>
/// Ushort change event args
/// </summary>
public class UshrtChangeEventArgs : EventArgs
{
/// <summary>
/// Ushort change event args integer value
/// </summary>
public ushort IntValue { get; set; }
/// <summary>
/// Ushort change event args type
/// </summary>
public ushort Type { get; set; }
/// <summary>
/// Ushort change event args index
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public UshrtChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="intValue"></param>
/// <param name="type"></param>
public UshrtChangeEventArgs(ushort intValue, ushort type)
{
IntValue = intValue;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="intValue"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public UshrtChangeEventArgs(ushort intValue, ushort type, ushort index)
{
IntValue = intValue;
Type = type;
Index = index;
}
}
/// <summary>
/// String change event args
/// </summary>
public class StringChangeEventArgs : EventArgs
{
/// <summary>
/// String change event args value
/// </summary>
public string StringValue { get; set; }
/// <summary>
/// String change event args type
/// </summary>
public ushort Type { get; set; }
/// <summary>
/// string change event args index
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public StringChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="stringValue"></param>
/// <param name="type"></param>
public StringChangeEventArgs(string stringValue, ushort type)
{
StringValue = stringValue;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="stringValue"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public StringChangeEventArgs(string stringValue, ushort type, ushort index)
{
StringValue = stringValue;
Type = type;
Index = index;
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.GenericRESTfulCommunications
{
/// <summary>
/// Constants
/// </summary>
public class GenericRESTfulConstants
{
/// <summary>
/// Generic boolean change
/// </summary>
public const ushort BoolValueChange = 1;
/// <summary>
/// Generic Ushort change
/// </summary>
public const ushort UshrtValueChange = 101;
/// <summary>
/// Response Code Ushort change
/// </summary>
public const ushort ResponseCodeChange = 102;
/// <summary>
/// Generic String chagne
/// </summary>
public const ushort StringValueChange = 201;
/// <summary>
/// Response string change
/// </summary>
public const ushort ResponseStringChange = 202;
/// <summary>
/// Error string change
/// </summary>
public const ushort ErrorStringChange = 203;
}
}

View File

@@ -0,0 +1,256 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
using Crestron.SimplSharp.Net.Http;
using Crestron.SimplSharp.Net.Https;
namespace PepperDash.Core.GenericRESTfulCommunications
{
/// <summary>
/// Generic RESTful communication class
/// </summary>
public class GenericRESTfulClient
{
/// <summary>
/// Boolean event handler
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Ushort event handler
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
/// <summary>
/// String event handler
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Constructor
/// </summary>
public GenericRESTfulClient()
{
}
/// <summary>
/// Generic RESTful submit request
/// </summary>
/// <param name="url"></param>
/// <param name="port"></param>
/// <param name="requestType"></param>
/// <param name="username"></param>
/// <param name="password"></param>
/// <param name="contentType"></param>
public void SubmitRequest(string url, ushort port, ushort requestType, string contentType, string username, string password)
{
if (url.StartsWith("https:", StringComparison.OrdinalIgnoreCase))
{
SubmitRequestHttps(url, port, requestType, contentType, username, password);
}
else if (url.StartsWith("http:", StringComparison.OrdinalIgnoreCase))
{
SubmitRequestHttp(url, port, requestType, contentType, username, password);
}
else
{
OnStringChange(string.Format("Invalid URL {0}", url), 0, GenericRESTfulConstants.ErrorStringChange);
}
}
/// <summary>
/// Private HTTP submit request
/// </summary>
/// <param name="url"></param>
/// <param name="port"></param>
/// <param name="requestType"></param>
/// <param name="contentType"></param>
/// <param name="username"></param>
/// <param name="password"></param>
private void SubmitRequestHttp(string url, ushort port, ushort requestType, string contentType, string username, string password)
{
try
{
HttpClient client = new HttpClient();
HttpClientRequest request = new HttpClientRequest();
HttpClientResponse response;
client.KeepAlive = false;
if(port >= 1 || port <= 65535)
client.Port = port;
else
client.Port = 80;
var authorization = "";
if (!string.IsNullOrEmpty(username))
authorization = EncodeBase64(username, password);
if (!string.IsNullOrEmpty(authorization))
request.Header.SetHeaderValue("Authorization", authorization);
if (!string.IsNullOrEmpty(contentType))
request.Header.ContentType = contentType;
request.Url.Parse(url);
request.RequestType = (Crestron.SimplSharp.Net.Http.RequestType)requestType;
response = client.Dispatch(request);
CrestronConsole.PrintLine(string.Format("SubmitRequestHttp Response[{0}]: {1}", response.Code, response.ContentString.ToString()));
if (!string.IsNullOrEmpty(response.ContentString.ToString()))
OnStringChange(response.ContentString.ToString(), 0, GenericRESTfulConstants.ResponseStringChange);
if (response.Code > 0)
OnUshrtChange((ushort)response.Code, 0, GenericRESTfulConstants.ResponseCodeChange);
}
catch (Exception e)
{
//var msg = string.Format("SubmitRequestHttp({0}, {1}, {2}) failed:{3}", url, port, requestType, e.Message);
//CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
CrestronConsole.PrintLine(e.Message);
OnStringChange(e.Message, 0, GenericRESTfulConstants.ErrorStringChange);
}
}
/// <summary>
/// Private HTTPS submit request
/// </summary>
/// <param name="url"></param>
/// <param name="port"></param>
/// <param name="requestType"></param>
/// <param name="contentType"></param>
/// <param name="username"></param>
/// <param name="password"></param>
private void SubmitRequestHttps(string url, ushort port, ushort requestType, string contentType, string username, string password)
{
try
{
HttpsClient client = new HttpsClient();
HttpsClientRequest request = new HttpsClientRequest();
HttpsClientResponse response;
client.KeepAlive = false;
client.HostVerification = false;
client.PeerVerification = false;
var authorization = "";
if (!string.IsNullOrEmpty(username))
authorization = EncodeBase64(username, password);
if (!string.IsNullOrEmpty(authorization))
request.Header.SetHeaderValue("Authorization", authorization);
if (!string.IsNullOrEmpty(contentType))
request.Header.ContentType = contentType;
request.Url.Parse(url);
request.RequestType = (Crestron.SimplSharp.Net.Https.RequestType)requestType;
response = client.Dispatch(request);
CrestronConsole.PrintLine(string.Format("SubmitRequestHttp Response[{0}]: {1}", response.Code, response.ContentString.ToString()));
if(!string.IsNullOrEmpty(response.ContentString.ToString()))
OnStringChange(response.ContentString.ToString(), 0, GenericRESTfulConstants.ResponseStringChange);
if(response.Code > 0)
OnUshrtChange((ushort)response.Code, 0, GenericRESTfulConstants.ResponseCodeChange);
}
catch (Exception e)
{
//var msg = string.Format("SubmitRequestHttps({0}, {1}, {2}, {3}, {4}) failed:{5}", url, port, requestType, username, password, e.Message);
//CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
CrestronConsole.PrintLine(e.Message);
OnStringChange(e.Message, 0, GenericRESTfulConstants.ErrorStringChange);
}
}
/// <summary>
/// Private method to encode username and password to Base64 string
/// </summary>
/// <param name="username"></param>
/// <param name="password"></param>
/// <returns>authorization</returns>
private string EncodeBase64(string username, string password)
{
var authorization = "";
try
{
if (!string.IsNullOrEmpty(username))
{
string base64String = System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes(string.Format("{0}:{1}", username, password)));
authorization = string.Format("Basic {0}", base64String);
}
}
catch (Exception e)
{
var msg = string.Format("EncodeBase64({0}, {1}) failed:\r{2}", username, password, e);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
return "" ;
}
return authorization;
}
/// <summary>
/// Protected method to handle boolean change events
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// Protected mehtod to handle ushort change events
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUshrtChange(ushort value, ushort index, ushort type)
{
var handler = UshrtChange;
if (handler != null)
{
var args = new UshrtChangeEventArgs(value, type);
args.Index = index;
UshrtChange(this, args);
}
}
/// <summary>
/// Protected method to handle string change events
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
}
}

View File

@@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.JsonStandardObjects
{
/// <summary>
/// Constants for simpl modules
/// </summary>
public class JsonStandardDeviceConstants
{
/// <summary>
/// Json object evaluated constant
/// </summary>
public const ushort JsonObjectEvaluated = 2;
/// <summary>
/// Json object changed constant
/// </summary>
public const ushort JsonObjectChanged = 104;
}
/// <summary>
///
/// </summary>
public class DeviceChangeEventArgs : EventArgs
{
/// <summary>
/// Device change event args object
/// </summary>
public DeviceConfig Device { get; set; }
/// <summary>
/// Device change event args type
/// </summary>
public ushort Type { get; set; }
/// <summary>
/// Device change event args index
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public DeviceChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="device"></param>
/// <param name="type"></param>
public DeviceChangeEventArgs(DeviceConfig device, ushort type)
{
Device = device;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="device"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public DeviceChangeEventArgs(DeviceConfig device, ushort type, ushort index)
{
Device = device;
Type = type;
Index = index;
}
}
}

View File

@@ -0,0 +1,183 @@
using System;
using System.Linq;
using Crestron.SimplSharp;
using PepperDash.Core.JsonToSimpl;
using Serilog.Events;
namespace PepperDash.Core.JsonStandardObjects
{
/// <summary>
/// Device class
/// </summary>
public class DeviceConfig
{
/// <summary>
/// JSON config key property
/// </summary>
public string key { get; set; }
/// <summary>
/// JSON config name property
/// </summary>
public string name { get; set; }
/// <summary>
/// JSON config type property
/// </summary>
public string type { get; set; }
/// <summary>
/// JSON config properties
/// </summary>
public PropertiesConfig properties { get; set; }
/// <summary>
/// Bool change event handler
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Ushort change event handler
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
/// <summary>
/// String change event handler
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Object change event handler
/// </summary>
public event EventHandler<DeviceChangeEventArgs> DeviceChange;
/// <summary>
/// Constructor
/// </summary>
public DeviceConfig()
{
properties = new PropertiesConfig();
}
/// <summary>
/// Initialize method
/// </summary>
/// <param name="uniqueID"></param>
/// <param name="deviceKey"></param>
public void Initialize(string uniqueID, string deviceKey)
{
// S+ set EvaluateFb low
OnBoolChange(false, 0, JsonStandardDeviceConstants.JsonObjectEvaluated);
// validate parameters
if (string.IsNullOrEmpty(uniqueID) || string.IsNullOrEmpty(deviceKey))
{
Debug.LogMessage(LogEventLevel.Debug, "UniqueID ({0} or key ({1} is null or empty", uniqueID, deviceKey);
// S+ set EvaluteFb high
OnBoolChange(true, 0, JsonStandardDeviceConstants.JsonObjectEvaluated);
return;
}
key = deviceKey;
try
{
// get the file using the unique ID
JsonToSimplMaster jsonMaster = J2SGlobal.GetMasterByFile(uniqueID);
if (jsonMaster == null)
{
Debug.LogMessage(LogEventLevel.Debug, "Could not find JSON file with uniqueID {0}", uniqueID);
return;
}
// get the device configuration using the key
var devices = jsonMaster.JsonObject.ToObject<RootObject>().devices;
var device = devices.FirstOrDefault(d => d.key.Equals(key));
if (device == null)
{
Debug.LogMessage(LogEventLevel.Debug, "Could not find device with key {0}", key);
return;
}
OnObjectChange(device, 0, JsonStandardDeviceConstants.JsonObjectChanged);
var index = devices.IndexOf(device);
OnStringChange(string.Format("devices[{0}]", index), 0, JsonToSimplConstants.FullPathToArrayChange);
}
catch (Exception e)
{
var msg = string.Format("Device {0} lookup failed:\r{1}", key, e);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
}
finally
{
// S+ set EvaluteFb high
OnBoolChange(true, 0, JsonStandardDeviceConstants.JsonObjectEvaluated);
}
}
#region EventHandler Helpers
/// <summary>
/// BoolChange event handler helper
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// UshrtChange event handler helper
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUshrtChange(ushort state, ushort index, ushort type)
{
var handler = UshrtChange;
if (handler != null)
{
var args = new UshrtChangeEventArgs(state, type);
args.Index = index;
UshrtChange(this, args);
}
}
/// <summary>
/// StringChange event handler helper
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
/// <summary>
/// ObjectChange event handler helper
/// </summary>
/// <param name="device"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnObjectChange(DeviceConfig device, ushort index, ushort type)
{
if (DeviceChange != null)
{
var args = new DeviceChangeEventArgs(device, type);
args.Index = index;
DeviceChange(this, args);
}
}
#endregion EventHandler Helpers
}
}

View File

@@ -0,0 +1,257 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.JsonStandardObjects
{
/*
Convert JSON snippt to C#: http://json2csharp.com/#
JSON Snippet:
{
"devices": [
{
"key": "deviceKey",
"name": "deviceName",
"type": "deviceType",
"properties": {
"deviceId": 1,
"enabled": true,
"control": {
"method": "methodName",
"controlPortDevKey": "deviceControlPortDevKey",
"controlPortNumber": 1,
"comParams": {
"baudRate": 9600,
"dataBits": 8,
"stopBits": 1,
"parity": "None",
"protocol": "RS232",
"hardwareHandshake": "None",
"softwareHandshake": "None",
"pacing": 0
},
"tcpSshProperties": {
"address": "172.22.1.101",
"port": 23,
"username": "user01",
"password": "password01",
"autoReconnect": false,
"autoReconnectIntervalMs": 10000
}
}
}
}
]
}
*/
/// <summary>
/// Device communication parameter class
/// </summary>
public class ComParamsConfig
{
/// <summary>
///
/// </summary>
public int baudRate { get; set; }
/// <summary>
///
/// </summary>
public int dataBits { get; set; }
/// <summary>
///
/// </summary>
public int stopBits { get; set; }
/// <summary>
///
/// </summary>
public string parity { get; set; }
/// <summary>
///
/// </summary>
public string protocol { get; set; }
/// <summary>
///
/// </summary>
public string hardwareHandshake { get; set; }
/// <summary>
///
/// </summary>
public string softwareHandshake { get; set; }
/// <summary>
///
/// </summary>
public int pacing { get; set; }
// convert properties for simpl
/// <summary>
///
/// </summary>
public ushort simplBaudRate { get { return Convert.ToUInt16(baudRate); } }
/// <summary>
///
/// </summary>
public ushort simplDataBits { get { return Convert.ToUInt16(dataBits); } }
/// <summary>
///
/// </summary>
public ushort simplStopBits { get { return Convert.ToUInt16(stopBits); } }
/// <summary>
///
/// </summary>
public ushort simplPacing { get { return Convert.ToUInt16(pacing); } }
/// <summary>
/// Constructor
/// </summary>
public ComParamsConfig()
{
}
}
/// <summary>
/// Device TCP/SSH properties class
/// </summary>
public class TcpSshPropertiesConfig
{
/// <summary>
///
/// </summary>
public string address { get; set; }
/// <summary>
///
/// </summary>
public int port { get; set; }
/// <summary>
///
/// </summary>
public string username { get; set; }
/// <summary>
///
/// </summary>
public string password { get; set; }
/// <summary>
///
/// </summary>
public bool autoReconnect { get; set; }
/// <summary>
///
/// </summary>
public int autoReconnectIntervalMs { get; set; }
// convert properties for simpl
/// <summary>
///
/// </summary>
public ushort simplPort { get { return Convert.ToUInt16(port); } }
/// <summary>
///
/// </summary>
public ushort simplAutoReconnect { get { return (ushort)(autoReconnect ? 1 : 0); } }
/// <summary>
///
/// </summary>
public ushort simplAutoReconnectIntervalMs { get { return Convert.ToUInt16(autoReconnectIntervalMs); } }
/// <summary>
/// Constructor
/// </summary>
public TcpSshPropertiesConfig()
{
}
}
/// <summary>
/// Device control class
/// </summary>
public class ControlConfig
{
/// <summary>
///
/// </summary>
public string method { get; set; }
/// <summary>
///
/// </summary>
public string controlPortDevKey { get; set; }
/// <summary>
///
/// </summary>
public int controlPortNumber { get; set; }
/// <summary>
///
/// </summary>
public ComParamsConfig comParams { get; set; }
/// <summary>
///
/// </summary>
public TcpSshPropertiesConfig tcpSshProperties { get; set; }
// convert properties for simpl
/// <summary>
///
/// </summary>
public ushort simplControlPortNumber { get { return Convert.ToUInt16(controlPortNumber); } }
/// <summary>
/// Constructor
/// </summary>
public ControlConfig()
{
comParams = new ComParamsConfig();
tcpSshProperties = new TcpSshPropertiesConfig();
}
}
/// <summary>
/// Device properties class
/// </summary>
public class PropertiesConfig
{
/// <summary>
///
/// </summary>
public int deviceId { get; set; }
/// <summary>
///
/// </summary>
public bool enabled { get; set; }
/// <summary>
///
/// </summary>
public ControlConfig control { get; set; }
// convert properties for simpl
/// <summary>
///
/// </summary>
public ushort simplDeviceId { get { return Convert.ToUInt16(deviceId); } }
/// <summary>
///
/// </summary>
public ushort simplEnabled { get { return (ushort)(enabled ? 1 : 0); } }
/// <summary>
/// Constructor
/// </summary>
public PropertiesConfig()
{
control = new ControlConfig();
}
}
/// <summary>
/// Root device class
/// </summary>
public class RootObject
{
/// <summary>
/// The collection of devices
/// </summary>
public List<DeviceConfig> devices { get; set; }
}
}

View File

@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Constants for Simpl modules
/// </summary>
public class JsonToSimplConstants
{
/// <summary>
///
/// </summary>
public const ushort BoolValueChange = 1;
/// <summary>
///
/// </summary>
public const ushort JsonIsValidBoolChange = 2;
/// <summary>
/// Reports the if the device is 3-series compatible
/// </summary>
public const ushort ProgramCompatibility3SeriesChange = 3;
/// <summary>
/// Reports the if the device is 4-series compatible
/// </summary>
public const ushort ProgramCompatibility4SeriesChange = 4;
/// <summary>
/// Reports the device platform enum value
/// </summary>
public const ushort DevicePlatformValueChange = 5;
/// <summary>
///
/// </summary>
public const ushort UshortValueChange = 101;
/// <summary>
///
/// </summary>
public const ushort StringValueChange = 201;
/// <summary>
///
/// </summary>
public const ushort FullPathToArrayChange = 202;
/// <summary>
///
/// </summary>
public const ushort ActualFilePathChange = 203;
/// <summary>
///
/// </summary>
public const ushort FilenameResolvedChange = 204;
/// <summary>
///
/// </summary>
public const ushort FilePathResolvedChange = 205;
/// <summary>
/// Reports the root directory change
/// </summary>
public const ushort RootDirectoryChange = 206;
/// <summary>
/// Reports the room ID change
/// </summary>
public const ushort RoomIdChange = 207;
/// <summary>
/// Reports the room name change
/// </summary>
public const ushort RoomNameChange = 208;
}
/// <summary>
/// S+ values delegate
/// </summary>
public delegate void SPlusValuesDelegate();
/// <summary>
/// S+ values wrapper
/// </summary>
public class SPlusValueWrapper
{
/// <summary>
///
/// </summary>
public SPlusType ValueType { get; private set; }
/// <summary>
///
/// </summary>
public ushort Index { get; private set; }
/// <summary>
///
/// </summary>
public ushort BoolUShortValue { get; set; }
/// <summary>
///
/// </summary>
public string StringValue { get; set; }
/// <summary>
///
/// </summary>
public SPlusValueWrapper() {}
/// <summary>
///
/// </summary>
/// <param name="type"></param>
/// <param name="index"></param>
public SPlusValueWrapper(SPlusType type, ushort index)
{
ValueType = type;
Index = index;
}
}
/// <summary>
/// S+ types enum
/// </summary>
public enum SPlusType
{
/// <summary>
/// Digital
/// </summary>
Digital,
/// <summary>
/// Analog
/// </summary>
Analog,
/// <summary>
/// String
/// </summary>
String
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
//using PepperDash.Core;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// The global class to manage all the instances of JsonToSimplMaster
/// </summary>
public class J2SGlobal
{
static List<JsonToSimplMaster> Masters = new List<JsonToSimplMaster>();
/// <summary>
/// Adds a file master. If the master's key or filename is equivalent to any existing
/// master, this will fail
/// </summary>
/// <param name="master">New master to add</param>
///
public static void AddMaster(JsonToSimplMaster master)
{
if (master == null)
throw new ArgumentNullException("master");
if (string.IsNullOrEmpty(master.UniqueID))
throw new InvalidOperationException("JSON Master cannot be added with a null UniqueId");
Debug.Console(1, "JSON Global adding master {0}", master.UniqueID);
if (Masters.Contains(master)) return;
var existing = Masters.FirstOrDefault(m =>
m.UniqueID.Equals(master.UniqueID, StringComparison.OrdinalIgnoreCase));
if (existing == null)
{
Masters.Add(master);
}
else
{
var msg = string.Format("Cannot add JSON Master with unique ID '{0}'.\rID is already in use on another master.", master.UniqueID);
CrestronConsole.PrintLine(msg);
ErrorLog.Warn(msg);
}
}
/// <summary>
/// Gets a master by its key. Case-insensitive
/// </summary>
public static JsonToSimplMaster GetMasterByFile(string file)
{
return Masters.FirstOrDefault(m => m.UniqueID.Equals(file, StringComparison.OrdinalIgnoreCase));
}
}
}

View File

@@ -0,0 +1,162 @@
using System;
using System.Linq;
using Newtonsoft.Json.Linq;
using Serilog.Events;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Used to interact with an array of values with the S+ modules
/// </summary>
public class JsonToSimplArrayLookupChild : JsonToSimplChildObjectBase
{
/// <summary>
///
/// </summary>
public string SearchPropertyName { get; set; }
/// <summary>
///
/// </summary>
public string SearchPropertyValue { get; set; }
int ArrayIndex;
/// <summary>
/// For gt2.4.1 array lookups
/// </summary>
/// <param name="file"></param>
/// <param name="key"></param>
/// <param name="pathPrefix"></param>
/// <param name="pathSuffix"></param>
/// <param name="searchPropertyName"></param>
/// <param name="searchPropertyValue"></param>
public void Initialize(string file, string key, string pathPrefix, string pathSuffix,
string searchPropertyName, string searchPropertyValue)
{
base.Initialize(file, key, pathPrefix, pathSuffix);
SearchPropertyName = searchPropertyName;
SearchPropertyValue = searchPropertyValue;
}
/// <summary>
/// For newer >=2.4.1 array lookups.
/// </summary>
/// <param name="file"></param>
/// <param name="key"></param>
/// <param name="pathPrefix"></param>
/// <param name="pathAppend"></param>
/// <param name="pathSuffix"></param>
/// <param name="searchPropertyName"></param>
/// <param name="searchPropertyValue"></param>
public void InitializeWithAppend(string file, string key, string pathPrefix, string pathAppend,
string pathSuffix, string searchPropertyName, string searchPropertyValue)
{
string pathPrefixWithAppend = (pathPrefix != null ? pathPrefix : "") + GetPathAppend(pathAppend);
base.Initialize(file, key, pathPrefixWithAppend, pathSuffix);
SearchPropertyName = searchPropertyName;
SearchPropertyValue = searchPropertyValue;
}
//PathPrefix+ArrayName+[x]+path+PathSuffix
/// <summary>
///
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
protected override string GetFullPath(string path)
{
return string.Format("{0}[{1}].{2}{3}",
PathPrefix == null ? "" : PathPrefix,
ArrayIndex,
path,
PathSuffix == null ? "" : PathSuffix);
}
/// <summary>
/// Process all values
/// </summary>
public override void ProcessAll()
{
if (FindInArray())
base.ProcessAll();
}
/// <summary>
/// Provides the path append for GetFullPath
/// </summary>
/// <returns></returns>
string GetPathAppend(string a)
{
if (string.IsNullOrEmpty(a))
{
return "";
}
if (a.StartsWith("."))
{
return a;
}
else
{
return "." + a;
}
}
/// <summary>
///
/// </summary>
/// <returns></returns>
bool FindInArray()
{
if (Master == null)
throw new InvalidOperationException("Cannot do operations before master is linked");
if (Master.JsonObject == null)
throw new InvalidOperationException("Cannot do operations before master JSON has read");
if (PathPrefix == null)
throw new InvalidOperationException("Cannot do operations before PathPrefix is set");
var token = Master.JsonObject.SelectToken(PathPrefix);
if (token is JArray)
{
var array = token as JArray;
try
{
var item = array.FirstOrDefault(o =>
{
var prop = o[SearchPropertyName];
return prop != null && prop.Value<string>()
.Equals(SearchPropertyValue, StringComparison.OrdinalIgnoreCase);
});
if (item == null)
{
Debug.LogMessage(LogEventLevel.Debug,"JSON Child[{0}] Array '{1}' '{2}={3}' not found: ", Key,
PathPrefix, SearchPropertyName, SearchPropertyValue);
this.LinkedToObject = false;
return false;
}
this.LinkedToObject = true;
ArrayIndex = array.IndexOf(item);
OnStringChange(string.Format("{0}[{1}]", PathPrefix, ArrayIndex), 0, JsonToSimplConstants.FullPathToArrayChange);
Debug.LogMessage(LogEventLevel.Debug, "JSON Child[{0}] Found array match at index {1}", Key, ArrayIndex);
return true;
}
catch (Exception e)
{
Debug.LogMessage(e, "JSON Child[{key}] Array '{pathPrefix}' lookup error: '{searchPropertyName}={searchPropertyValue}'", null, Key,
PathPrefix, SearchPropertyName, SearchPropertyValue, e);
}
}
else
{
Debug.LogMessage(LogEventLevel.Debug, "JSON Child[{0}] Path '{1}' is not an array", Key, PathPrefix);
}
return false;
}
}
}

View File

@@ -0,0 +1,404 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Base class for JSON objects
/// </summary>
public abstract class JsonToSimplChildObjectBase : IKeyed
{
/// <summary>
/// Notifies of bool change
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Notifies of ushort change
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UShortChange;
/// <summary>
/// Notifies of string change
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Delegate to get all values
/// </summary>
public SPlusValuesDelegate GetAllValuesDelegate { get; set; }
/// <summary>
/// Use a callback to reduce task switch/threading
/// </summary>
public SPlusValuesDelegate SetAllPathsDelegate { get; set; }
/// <summary>
/// Unique identifier for instance
/// </summary>
public string Key { get; protected set; }
/// <summary>
/// This will be prepended to all paths to allow path swapping or for more organized
/// sub-paths
/// </summary>
public string PathPrefix { get; protected set; }
/// <summary>
/// This is added to the end of all paths
/// </summary>
public string PathSuffix { get; protected set; }
/// <summary>
/// Indicates if the instance is linked to an object
/// </summary>
public bool LinkedToObject { get; protected set; }
/// <summary>
/// Reference to Master instance
/// </summary>
protected JsonToSimplMaster Master;
/// <summary>
/// Paths to boolean values in JSON structure
/// </summary>
protected Dictionary<ushort, string> BoolPaths = new Dictionary<ushort, string>();
/// <summary>
/// Paths to numeric values in JSON structure
/// </summary>
protected Dictionary<ushort, string> UshortPaths = new Dictionary<ushort, string>();
/// <summary>
/// Paths to string values in JSON structure
/// </summary>
protected Dictionary<ushort, string> StringPaths = new Dictionary<ushort, string>();
/// <summary>
/// Call this before doing anything else
/// </summary>
/// <param name="masterUniqueId"></param>
/// <param name="key"></param>
/// <param name="pathPrefix"></param>
/// <param name="pathSuffix"></param>
public void Initialize(string masterUniqueId, string key, string pathPrefix, string pathSuffix)
{
Key = key;
PathPrefix = pathPrefix;
PathSuffix = pathSuffix;
Master = J2SGlobal.GetMasterByFile(masterUniqueId);
if (Master != null)
Master.AddChild(this);
else
Debug.Console(1, "JSON Child [{0}] cannot link to master {1}", key, masterUniqueId);
}
/// <summary>
/// Sets the path prefix for the object
/// </summary>
/// <param name="pathPrefix"></param>
public void SetPathPrefix(string pathPrefix)
{
PathPrefix = pathPrefix;
}
/// <summary>
/// Set the JPath to evaluate for a given bool out index.
/// </summary>
public void SetBoolPath(ushort index, string path)
{
Debug.Console(1, "JSON Child[{0}] SetBoolPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return;
BoolPaths[index] = path;
}
/// <summary>
/// Set the JPath for a ushort out index.
/// </summary>
public void SetUshortPath(ushort index, string path)
{
Debug.Console(1, "JSON Child[{0}] SetUshortPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return;
UshortPaths[index] = path;
}
/// <summary>
/// Set the JPath for a string output index.
/// </summary>
public void SetStringPath(ushort index, string path)
{
Debug.Console(1, "JSON Child[{0}] SetStringPath {1}={2}", Key, index, path);
if (path == null || path.Trim() == string.Empty) return;
StringPaths[index] = path;
}
/// <summary>
/// Evalutates all outputs with defined paths. called by S+ when paths are ready to process
/// and by Master when file is read.
/// </summary>
public virtual void ProcessAll()
{
if (!LinkedToObject)
{
Debug.Console(1, this, "Not linked to object in file. Skipping");
return;
}
if (SetAllPathsDelegate == null)
{
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring ProcessAll");
return;
}
SetAllPathsDelegate();
foreach (var kvp in BoolPaths)
ProcessBoolPath(kvp.Key);
foreach (var kvp in UshortPaths)
ProcessUshortPath(kvp.Key);
foreach (var kvp in StringPaths)
ProcessStringPath(kvp.Key);
}
/// <summary>
/// Processes a bool property, converting to bool, firing off a BoolChange event
/// </summary>
void ProcessBoolPath(ushort index)
{
string response;
if (Process(BoolPaths[index], out response))
OnBoolChange(response.Equals("true", StringComparison.OrdinalIgnoreCase),
index, JsonToSimplConstants.BoolValueChange);
else { }
// OnBoolChange(false, index, JsonToSimplConstants.BoolValueChange);
}
// Processes the path to a ushort, converting to ushort if able, twos complement if necessary, firing off UshrtChange event
void ProcessUshortPath(ushort index) {
string response;
if (Process(UshortPaths[index], out response)) {
ushort val;
try { val = Convert.ToInt32(response) < 0 ? (ushort)(Convert.ToInt16(response) + 65536) : Convert.ToUInt16(response); }
catch { val = 0; }
OnUShortChange(val, index, JsonToSimplConstants.UshortValueChange);
}
else { }
// OnUShortChange(0, index, JsonToSimplConstants.UshortValueChange);
}
// Processes the path to a string property and fires of a StringChange event.
void ProcessStringPath(ushort index)
{
string response;
if (Process(StringPaths[index], out response))
OnStringChange(response, index, JsonToSimplConstants.StringValueChange);
else { }
// OnStringChange("", index, JsonToSimplConstants.StringValueChange);
}
/// <summary>
/// Processes the given path.
/// </summary>
/// <param name="path">JPath formatted path to the desired property</param>
/// <param name="response">The string value of the property, or a default value if it
/// doesn't exist</param>
/// <returns> This will return false in the case that EvaulateAllOnJsonChange
/// is false and the path does not evaluate to a property in the incoming JSON. </returns>
bool Process(string path, out string response)
{
path = GetFullPath(path);
Debug.Console(1, "JSON Child[{0}] Processing {1}", Key, path);
response = "";
if (Master == null)
{
Debug.Console(1, "JSONChild[{0}] cannot process without Master attached", Key);
return false;
}
if (Master.JsonObject != null && path != string.Empty)
{
bool isCount = false;
path = path.Trim();
if (path.EndsWith(".Count"))
{
path = path.Remove(path.Length - 6, 6);
isCount = true;
}
try // Catch a strange cast error on a bad path
{
var t = Master.JsonObject.SelectToken(path);
if (t != null)
{
// return the count of children objects - if any
if (isCount)
response = (t.HasValues ? t.Children().Count() : 0).ToString();
else
response = t.Value<string>();
Debug.Console(1, " ='{0}'", response);
return true;
}
}
catch
{
response = "";
}
}
// If the path isn't found, return this to determine whether to pass out the non-value or not.
return false;
}
//************************************************************************************************
// Save-related functions
/// <summary>
/// Called from Master to read inputs and update their values in master JObject
/// Callback should hit one of the following four methods
/// </summary>
public void UpdateInputsForMaster()
{
if (!LinkedToObject)
{
Debug.Console(1, this, "Not linked to object in file. Skipping");
return;
}
if (SetAllPathsDelegate == null)
{
Debug.Console(1, this, "No SetAllPathsDelegate set. Ignoring UpdateInputsForMaster");
return;
}
SetAllPathsDelegate();
var del = GetAllValuesDelegate;
if (del != null)
GetAllValuesDelegate();
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="theValue"></param>
public void USetBoolValue(ushort key, ushort theValue)
{
SetBoolValue(key, theValue == 1);
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="theValue"></param>
public void SetBoolValue(ushort key, bool theValue)
{
if (BoolPaths.ContainsKey(key))
SetValueOnMaster(BoolPaths[key], new JValue(theValue));
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="theValue"></param>
public void SetUShortValue(ushort key, ushort theValue)
{
if (UshortPaths.ContainsKey(key))
SetValueOnMaster(UshortPaths[key], new JValue(theValue));
}
/// <summary>
///
/// </summary>
/// <param name="key"></param>
/// <param name="theValue"></param>
public void SetStringValue(ushort key, string theValue)
{
if (StringPaths.ContainsKey(key))
SetValueOnMaster(StringPaths[key], new JValue(theValue));
}
/// <summary>
///
/// </summary>
/// <param name="keyPath"></param>
/// <param name="valueToSave"></param>
public void SetValueOnMaster(string keyPath, JValue valueToSave)
{
var path = GetFullPath(keyPath);
try
{
Debug.Console(1, "JSON Child[{0}] Queueing value on master {1}='{2}'", Key, path, valueToSave);
//var token = Master.JsonObject.SelectToken(path);
//if (token != null) // The path exists in the file
Master.AddUnsavedValue(path, valueToSave);
}
catch (Exception e)
{
Debug.Console(1, "JSON Child[{0}] Failed setting value for path '{1}'\r{2}", Key, path, e);
}
}
/// <summary>
/// Called during Process(...) to get the path to a given property. By default,
/// returns PathPrefix+path+PathSuffix. Override to change the way path is built.
/// </summary>
protected virtual string GetFullPath(string path)
{
return (PathPrefix != null ? PathPrefix : "") +
path + (PathSuffix != null ? PathSuffix : "");
}
// Helpers for events
//******************************************************************************************
/// <summary>
/// Event helper
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
//******************************************************************************************
/// <summary>
/// Event helper
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUShortChange(ushort state, ushort index, ushort type)
{
var handler = UShortChange;
if (handler != null)
{
var args = new UshrtChangeEventArgs(state, type);
args.Index = index;
UShortChange(this, args);
}
}
/// <summary>
/// Event helper
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
}
}

View File

@@ -0,0 +1,289 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Represents a JSON file that can be read and written to
/// </summary>
public class JsonToSimplFileMaster : JsonToSimplMaster
{
/// <summary>
/// Sets the filepath as well as registers this with the Global.Masters list
/// </summary>
public string Filepath { get; private set; }
/// <summary>
/// Filepath to the actual file that will be read (Portal or local)
/// </summary>
public string ActualFilePath { get; private set; }
/// <summary>
///
/// </summary>
public string Filename { get; private set; }
/// <summary>
///
/// </summary>
public string FilePathName { get; private set; }
/*****************************************************************************************/
/** Privates **/
// The JSON file in JObject form
// For gathering the incoming data
object StringBuilderLock = new object();
// To prevent multiple same-file access
static object FileLock = new object();
/*****************************************************************************************/
/// <summary>
/// SIMPL+ default constructor.
/// </summary>
public JsonToSimplFileMaster()
{
}
/// <summary>
/// Read, evaluate and udpate status
/// </summary>
public void EvaluateFile(string filepath)
{
try
{
OnBoolChange(false, 0, JsonToSimplConstants.JsonIsValidBoolChange);
var dirSeparator = Path.DirectorySeparatorChar;
var dirSeparatorAlt = Path.AltDirectorySeparatorChar;
var series = CrestronEnvironment.ProgramCompatibility;
var is3Series = (eCrestronSeries.Series3 == (series & eCrestronSeries.Series3));
OnBoolChange(is3Series, 0,
JsonToSimplConstants.ProgramCompatibility3SeriesChange);
var is4Series = (eCrestronSeries.Series4 == (series & eCrestronSeries.Series4));
OnBoolChange(is4Series, 0,
JsonToSimplConstants.ProgramCompatibility4SeriesChange);
var isServer = CrestronEnvironment.DevicePlatform == eDevicePlatform.Server;
OnBoolChange(isServer, 0,
JsonToSimplConstants.DevicePlatformValueChange);
// get the roomID
var roomId = Crestron.SimplSharp.InitialParametersClass.RoomId;
if (!string.IsNullOrEmpty(roomId))
{
OnStringChange(roomId, 0, JsonToSimplConstants.RoomIdChange);
}
// get the roomName
var roomName = Crestron.SimplSharp.InitialParametersClass.RoomName;
if (!string.IsNullOrEmpty(roomName))
{
OnStringChange(roomName, 0, JsonToSimplConstants.RoomNameChange);
}
var rootDirectory = Directory.GetApplicationRootDirectory();
OnStringChange(rootDirectory, 0, JsonToSimplConstants.RootDirectoryChange);
var splusPath = string.Empty;
if (Regex.IsMatch(filepath, @"user", RegexOptions.IgnoreCase))
{
if (is4Series)
splusPath = Regex.Replace(filepath, "user", "user", RegexOptions.IgnoreCase);
else if (isServer)
splusPath = Regex.Replace(filepath, "user", "User", RegexOptions.IgnoreCase);
else
splusPath = filepath;
}
filepath = splusPath.Replace(dirSeparatorAlt, dirSeparator);
Filepath = string.Format("{1}{0}{2}", dirSeparator, rootDirectory,
filepath.TrimStart(dirSeparator, dirSeparatorAlt));
OnStringChange(string.Format("Attempting to evaluate {0}", Filepath), 0, JsonToSimplConstants.StringValueChange);
if (string.IsNullOrEmpty(Filepath))
{
OnStringChange(string.Format("Cannot evaluate file. JSON file path not set"), 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine("Cannot evaluate file. JSON file path not set");
return;
}
// get file directory and name to search
var fileDirectory = Path.GetDirectoryName(Filepath);
var fileName = Path.GetFileName(Filepath);
OnStringChange(string.Format("Checking '{0}' for '{1}'", fileDirectory, fileName), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "Checking '{0}' for '{1}'", fileDirectory, fileName);
if (Directory.Exists(fileDirectory))
{
// get the directory info
var directoryInfo = new DirectoryInfo(fileDirectory);
// get the file to be read
var actualFile = directoryInfo.GetFiles(fileName).FirstOrDefault();
if (actualFile == null)
{
var msg = string.Format("JSON file not found: {0}", Filepath);
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
return;
}
// \xSE\xR\PDT000-Template_Main_Config-Combined_DSP_v00.02.json
// \USER\PDT000-Template_Main_Config-Combined_DSP_v00.02.json
ActualFilePath = actualFile.FullName;
OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange);
OnStringChange(string.Format("Actual JSON file is {0}", ActualFilePath), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "Actual JSON file is {0}", ActualFilePath);
Filename = actualFile.Name;
OnStringChange(Filename, 0, JsonToSimplConstants.FilenameResolvedChange);
OnStringChange(string.Format("JSON Filename is {0}", Filename), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "JSON Filename is {0}", Filename);
FilePathName = string.Format(@"{0}{1}", actualFile.DirectoryName, dirSeparator);
OnStringChange(string.Format(@"{0}", actualFile.DirectoryName), 0, JsonToSimplConstants.FilePathResolvedChange);
OnStringChange(string.Format(@"JSON File Path is {0}", actualFile.DirectoryName), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "JSON File Path is {0}", FilePathName);
var json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII);
JsonObject = JObject.Parse(json);
foreach (var child in Children)
child.ProcessAll();
OnBoolChange(true, 0, JsonToSimplConstants.JsonIsValidBoolChange);
}
else
{
OnStringChange(string.Format("'{0}' not found", fileDirectory), 0, JsonToSimplConstants.StringValueChange);
Debug.Console(1, "'{0}' not found", fileDirectory);
}
}
catch (Exception e)
{
var msg = string.Format("EvaluateFile Exception: Message\r{0}", e.Message);
OnStringChange(msg, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
var stackTrace = string.Format("EvaluateFile: Stack Trace\r{0}", e.StackTrace);
OnStringChange(stackTrace, 0, JsonToSimplConstants.StringValueChange);
CrestronConsole.PrintLine(stackTrace);
ErrorLog.Error(stackTrace);
}
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"></param>
public void setDebugLevel(uint level)
{
Debug.SetDebugLevel(level);
}
/// <summary>
/// Saves the values to the file
/// </summary>
public override void Save()
{
// this code is duplicated in the other masters!!!!!!!!!!!!!
UnsavedValues = new Dictionary<string, JValue>();
// Make each child update their values into master object
foreach (var child in Children)
{
Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
child.UpdateInputsForMaster();
}
if (UnsavedValues == null || UnsavedValues.Count == 0)
{
Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID);
return;
}
lock (FileLock)
{
Debug.Console(1, "Saving");
foreach (var path in UnsavedValues.Keys)
{
var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null)
{// It's found
tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path);
}
else // No token. Let's make one
{
//http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net
Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
// JContainer jpart = JsonObject;
// // walk down the path and find where it goes
//#warning Does not handle arrays.
// foreach (var part in path.Split('.'))
// {
// var openPos = part.IndexOf('[');
// if (openPos > -1)
// {
// openPos++; // move to number
// var closePos = part.IndexOf(']');
// var arrayName = part.Substring(0, openPos - 1); // get the name
// var index = Convert.ToInt32(part.Substring(openPos, closePos - openPos));
// // Check if the array itself exists and add the item if so
// if (jpart[arrayName] != null)
// {
// var arrayObj = jpart[arrayName] as JArray;
// var item = arrayObj[index];
// if (item == null)
// arrayObj.Add(new JObject());
// }
// Debug.Console(0, "IGNORING MISSING ARRAY VALUE FOR NOW");
// continue;
// }
// // Build the
// if (jpart[part] == null)
// jpart.Add(new JProperty(part, new JObject()));
// jpart = jpart[part] as JContainer;
// }
// jpart.Replace(UnsavedValues[path]);
}
}
using (StreamWriter sw = new StreamWriter(ActualFilePath))
{
try
{
sw.Write(JsonObject.ToString());
sw.Flush();
}
catch (Exception e)
{
string err = string.Format("Error writing JSON file:\r{0}", e);
Debug.Console(0, err);
ErrorLog.Warn(err);
return;
}
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
///
/// </summary>
public class JsonToSimplFixedPathObject : JsonToSimplChildObjectBase
{
/// <summary>
/// Constructor
/// </summary>
public JsonToSimplFixedPathObject()
{
this.LinkedToObject = true;
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Generic Master
/// </summary>
public class JsonToSimplGenericMaster : JsonToSimplMaster
{
/*****************************************************************************************/
/** Privates **/
// The JSON file in JObject form
// For gathering the incoming data
object StringBuilderLock = new object();
// To prevent multiple same-file access
static object WriteLock = new object();
/// <summary>
/// Callback action for saving
/// </summary>
public Action<string> SaveCallback { get; set; }
/*****************************************************************************************/
/// <summary>
/// SIMPL+ default constructor.
/// </summary>
public JsonToSimplGenericMaster()
{
}
/// <summary>
/// Loads in JSON and triggers evaluation on all children
/// </summary>
/// <param name="json"></param>
public void LoadWithJson(string json)
{
OnBoolChange(false, 0, JsonToSimplConstants.JsonIsValidBoolChange);
try
{
JsonObject = JObject.Parse(json);
foreach (var child in Children)
child.ProcessAll();
OnBoolChange(true, 0, JsonToSimplConstants.JsonIsValidBoolChange);
}
catch (Exception e)
{
var msg = string.Format("JSON parsing failed:\r{0}", e);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
}
}
/// <summary>
/// Loads JSON into JsonObject, but does not trigger evaluation by children
/// </summary>
/// <param name="json"></param>
public void SetJsonWithoutEvaluating(string json)
{
try
{
JsonObject = JObject.Parse(json);
}
catch (Exception e)
{
Debug.Console(0, this, "JSON parsing failed:\r{0}", e);
}
}
/// <summary>
///
/// </summary>
public override void Save()
{
// this code is duplicated in the other masters!!!!!!!!!!!!!
UnsavedValues = new Dictionary<string, JValue>();
// Make each child update their values into master object
foreach (var child in Children)
{
Debug.Console(1, this, "Master. checking child [{0}] for updates to save", child.Key);
child.UpdateInputsForMaster();
}
if (UnsavedValues == null || UnsavedValues.Count == 0)
{
Debug.Console(1, this, "Master. No updated values to save. Skipping");
return;
}
lock (WriteLock)
{
Debug.Console(1, this, "Saving");
foreach (var path in UnsavedValues.Keys)
{
var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null)
{// It's found
tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, this, "Master Updating '{0}'", path);
}
else // No token. Let's make one
{
Debug.Console(1, "Master Cannot write value onto missing property: '{0}'", path);
}
}
}
if (SaveCallback != null)
SaveCallback(JsonObject.ToString());
else
Debug.Console(0, this, "WARNING: No save callback defined.");
}
}
}

View File

@@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.IO;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Abstract base class for JsonToSimpl interactions
/// </summary>
public abstract class JsonToSimplMaster : IKeyed
{
/// <summary>
/// Notifies of bool change
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Notifies of ushort change
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
/// <summary>
/// Notifies of string change
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// A collection of associated child modules
/// </summary>
protected List<JsonToSimplChildObjectBase> Children = new List<JsonToSimplChildObjectBase>();
/*****************************************************************************************/
/// <summary>
/// Mirrors the Unique ID for now.
/// </summary>
public string Key { get { return UniqueID; } }
/// <summary>
/// A unique ID
/// </summary>
public string UniqueID { get; protected set; }
/// <summary>
/// Merely for use in debug messages
/// </summary>
public string DebugName
{
get { return _DebugName; }
set { if (DebugName == null) _DebugName = ""; else _DebugName = value; }
}
string _DebugName = "";
/// <summary>
/// This will be prepended to all paths to allow path swapping or for more organized
/// sub-paths
/// </summary>
public string PathPrefix { get; set; }
/// <summary>
/// This is added to the end of all paths
/// </summary>
public string PathSuffix { get; set; }
/// <summary>
/// Enables debugging output to the console. Certain error messages will be logged to the
/// system's error log regardless of this setting
/// </summary>
public bool DebugOn { get; set; }
/// <summary>
/// Ushort helper for Debug property
/// </summary>
public ushort UDebug
{
get { return (ushort)(DebugOn ? 1 : 0); }
set
{
DebugOn = (value == 1);
CrestronConsole.PrintLine("JsonToSimpl debug={0}", DebugOn);
}
}
/// <summary>
///
/// </summary>
public JObject JsonObject { get; protected set; }
/*****************************************************************************************/
/** Privates **/
// The JSON file in JObject form
// For gathering the incoming data
protected Dictionary<string, JValue> UnsavedValues = new Dictionary<string, JValue>();
/*****************************************************************************************/
/// <summary>
/// SIMPL+ default constructor.
/// </summary>
public JsonToSimplMaster()
{
}
/// <summary>
/// Sets up class - overriding methods should always call this.
/// </summary>
/// <param name="uniqueId"></param>
public virtual void Initialize(string uniqueId)
{
UniqueID = uniqueId;
J2SGlobal.AddMaster(this); // Should not re-add
}
/// <summary>
/// Adds a child "module" to this master
/// </summary>
/// <param name="child"></param>
public void AddChild(JsonToSimplChildObjectBase child)
{
if (!Children.Contains(child))
{
Children.Add(child);
}
}
/// <summary>
/// Called from the child to add changed or new values for saving
/// </summary>
public void AddUnsavedValue(string path, JValue value)
{
if (UnsavedValues.ContainsKey(path))
{
Debug.Console(0, "Master[{0}] WARNING - Attempt to add duplicate value for path '{1}'.\r Ingoring. Please ensure that path does not exist on multiple modules.", UniqueID, path);
}
else
UnsavedValues.Add(path, value);
//Debug.Console(0, "Master[{0}] Unsaved size={1}", UniqueID, UnsavedValues.Count);
}
/// <summary>
/// Saves the file
/// </summary>
public abstract void Save();
/// <summary>
///
/// </summary>
public static class JsonFixes
{
/// <summary>
/// Deserializes a string into a JObject
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
public static JObject ParseObject(string json)
{
#if NET6_0
using (var reader = new JsonTextReader(new System.IO.StringReader(json)))
#else
using (var reader = new JsonTextReader(new Crestron.SimplSharp.CrestronIO.StringReader(json)))
#endif
{
var startDepth = reader.Depth;
var obj = JObject.Load(reader);
if (startDepth != reader.Depth)
throw new JsonSerializationException("Unenclosed json found");
return obj;
}
}
/// <summary>
/// Deserializes a string into a JArray
/// </summary>
/// <param name="json"></param>
/// <returns></returns>
public static JArray ParseArray(string json)
{
#if NET6_0
using (var reader = new JsonTextReader(new System.IO.StringReader(json)))
#else
using (var reader = new JsonTextReader(new Crestron.SimplSharp.CrestronIO.StringReader(json)))
#endif
{
var startDepth = reader.Depth;
var obj = JArray.Load(reader);
if (startDepth != reader.Depth)
throw new JsonSerializationException("Unenclosed json found");
return obj;
}
}
}
/// <summary>
/// Helper event
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
if (BoolChange != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// Helper event
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUshrtChange(ushort state, ushort index, ushort type)
{
if (UshrtChange != null)
{
var args = new UshrtChangeEventArgs(state, type);
args.Index = index;
UshrtChange(this, args);
}
}
/// <summary>
/// Helper event
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
if (StringChange != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
}
}

View File

@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json.Linq;
using PepperDash.Core.Config;
namespace PepperDash.Core.JsonToSimpl
{
/// <summary>
/// Portal File Master
/// </summary>
public class JsonToSimplPortalFileMaster : JsonToSimplMaster
{
/// <summary>
/// Sets the filepath as well as registers this with the Global.Masters list
/// </summary>
public string PortalFilepath { get; private set; }
/// <summary>
/// File path of the actual file being read (Portal or local)
/// </summary>
public string ActualFilePath { get; private set; }
/*****************************************************************************************/
/** Privates **/
// To prevent multiple same-file access
object StringBuilderLock = new object();
static object FileLock = new object();
/*****************************************************************************************/
/// <summary>
/// SIMPL+ default constructor.
/// </summary>
public JsonToSimplPortalFileMaster()
{
}
/// <summary>
/// Read, evaluate and udpate status
/// </summary>
public void EvaluateFile(string portalFilepath)
{
PortalFilepath = portalFilepath;
OnBoolChange(false, 0, JsonToSimplConstants.JsonIsValidBoolChange);
if (string.IsNullOrEmpty(PortalFilepath))
{
CrestronConsole.PrintLine("Cannot evaluate file. JSON file path not set");
return;
}
// Resolve possible wildcarded filename
// If the portal file is xyz.json, then
// the file we want to check for first will be called xyz.local.json
var localFilepath = Path.ChangeExtension(PortalFilepath, "local.json");
Debug.Console(0, this, "Checking for local file {0}", localFilepath);
var actualLocalFile = GetActualFileInfoFromPath(localFilepath);
if (actualLocalFile != null)
{
ActualFilePath = actualLocalFile.FullName;
OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange);
}
// If the local file does not exist, then read the portal file xyz.json
// and create the local.
else
{
Debug.Console(1, this, "Local JSON file not found {0}\rLoading portal JSON file", localFilepath);
var actualPortalFile = GetActualFileInfoFromPath(portalFilepath);
if (actualPortalFile != null)
{
var newLocalPath = Path.ChangeExtension(actualPortalFile.FullName, "local.json");
// got the portal file, hand off to the merge / save method
PortalConfigReader.ReadAndMergeFileIfNecessary(actualPortalFile.FullName, newLocalPath);
ActualFilePath = newLocalPath;
OnStringChange(ActualFilePath, 0, JsonToSimplConstants.ActualFilePathChange);
}
else
{
var msg = string.Format("Portal JSON file not found: {0}", PortalFilepath);
Debug.Console(1, this, msg);
ErrorLog.Error(msg);
return;
}
}
// At this point we should have a local file. Do it.
Debug.Console(1, "Reading local JSON file {0}", ActualFilePath);
string json = File.ReadToEnd(ActualFilePath, System.Text.Encoding.ASCII);
try
{
JsonObject = JObject.Parse(json);
foreach (var child in Children)
child.ProcessAll();
OnBoolChange(true, 0, JsonToSimplConstants.JsonIsValidBoolChange);
}
catch (Exception e)
{
var msg = string.Format("JSON parsing failed:\r{0}", e);
CrestronConsole.PrintLine(msg);
ErrorLog.Error(msg);
return;
}
}
/// <summary>
/// Returns the FileInfo object for a given path, with possible wildcards
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
FileInfo GetActualFileInfoFromPath(string path)
{
var dir = Path.GetDirectoryName(path);
var localFilename = Path.GetFileName(path);
var directory = new DirectoryInfo(dir);
// search the directory for the file w/ wildcards
return directory.GetFiles(localFilename).FirstOrDefault();
}
/// <summary>
///
/// </summary>
/// <param name="level"></param>
public void setDebugLevel(uint level)
{
Debug.SetDebugLevel(level);
}
/// <summary>
///
/// </summary>
public override void Save()
{
// this code is duplicated in the other masters!!!!!!!!!!!!!
UnsavedValues = new Dictionary<string, JValue>();
// Make each child update their values into master object
foreach (var child in Children)
{
Debug.Console(1, "Master [{0}] checking child [{1}] for updates to save", UniqueID, child.Key);
child.UpdateInputsForMaster();
}
if (UnsavedValues == null || UnsavedValues.Count == 0)
{
Debug.Console(1, "Master [{0}] No updated values to save. Skipping", UniqueID);
return;
}
lock (FileLock)
{
Debug.Console(1, "Saving");
foreach (var path in UnsavedValues.Keys)
{
var tokenToReplace = JsonObject.SelectToken(path);
if (tokenToReplace != null)
{// It's found
tokenToReplace.Replace(UnsavedValues[path]);
Debug.Console(1, "JSON Master[{0}] Updating '{1}'", UniqueID, path);
}
else // No token. Let's make one
{
//http://stackoverflow.com/questions/17455052/how-to-set-the-value-of-a-json-path-using-json-net
Debug.Console(1, "JSON Master[{0}] Cannot write value onto missing property: '{1}'", UniqueID, path);
}
}
using (StreamWriter sw = new StreamWriter(ActualFilePath))
{
try
{
sw.Write(JsonObject.ToString());
sw.Flush();
}
catch (Exception e)
{
string err = string.Format("Error writing JSON file:\r{0}", e);
Debug.Console(0, err);
ErrorLog.Warn(err);
return;
}
}
}
}
}
}

View File

@@ -0,0 +1,37 @@
using Crestron.SimplSharp;
using Serilog.Core;
using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Core.Logging
{
public class CrestronEnricher : ILogEventEnricher
{
static readonly string _appName;
static CrestronEnricher()
{
switch (CrestronEnvironment.DevicePlatform)
{
case eDevicePlatform.Appliance:
_appName = $"App {InitialParametersClass.ApplicationNumber}";
break;
case eDevicePlatform.Server:
_appName = $"{InitialParametersClass.RoomId}";
break;
}
}
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
var property = propertyFactory.CreateProperty("App", _appName);
logEvent.AddOrUpdateProperty(property);
}
}
}

View File

@@ -0,0 +1,895 @@
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronDataStore;
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.CrestronLogger;
using Newtonsoft.Json;
using PepperDash.Core.Logging;
using Serilog;
using Serilog.Context;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting.Compact;
using Serilog.Formatting.Json;
using Serilog.Templates;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text.RegularExpressions;
namespace PepperDash.Core
{
/// <summary>
/// Contains debug commands for use in various situations
/// </summary>
public static class Debug
{
private static readonly string LevelStoreKey = "ConsoleDebugLevel";
private static readonly string WebSocketLevelStoreKey = "WebsocketDebugLevel";
private static readonly string ErrorLogLevelStoreKey = "ErrorLogDebugLevel";
private static readonly string FileLevelStoreKey = "FileDebugLevel";
private static readonly Dictionary<uint, LogEventLevel> _logLevels = new Dictionary<uint, LogEventLevel>()
{
{0, LogEventLevel.Information },
{3, LogEventLevel.Warning },
{4, LogEventLevel.Error },
{5, LogEventLevel.Fatal },
{1, LogEventLevel.Debug },
{2, LogEventLevel.Verbose },
};
private static ILogger _logger;
private static readonly LoggingLevelSwitch _consoleLoggingLevelSwitch;
private static readonly LoggingLevelSwitch _websocketLoggingLevelSwitch;
private static readonly LoggingLevelSwitch _errorLogLevelSwitch;
private static readonly LoggingLevelSwitch _fileLevelSwitch;
public static LogEventLevel WebsocketMinimumLogLevel
{
get { return _websocketLoggingLevelSwitch.MinimumLevel; }
}
private static readonly DebugWebsocketSink _websocketSink;
public static DebugWebsocketSink WebsocketSink
{
get { return _websocketSink; }
}
/// <summary>
/// Describes the folder location where a given program stores it's debug level memory. By default, the
/// file written will be named appNdebug where N is 1-10.
/// </summary>
public static string OldFilePathPrefix = @"\nvram\debug\";
/// <summary>
/// Describes the new folder location where a given program stores it's debug level memory. By default, the
/// file written will be named appNdebug where N is 1-10.
/// </summary>
public static string NewFilePathPrefix = @"\user\debug\";
/// <summary>
/// The name of the file containing the current debug settings.
/// </summary>
public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber);
/// <summary>
/// Debug level to set for a given program.
/// </summary>
public static int Level { get; private set; }
/// <summary>
/// When this is true, the configuration file will NOT be loaded until triggered by either a console command or a signal
/// </summary>
public static bool DoNotLoadConfigOnNextBoot { get; private set; }
private static DebugContextCollection _contexts;
private const int SaveTimeoutMs = 30000;
public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance;
/// <summary>
/// Version for the currently loaded PepperDashCore dll
/// </summary>
public static string PepperDashCoreVersion { get; private set; }
private static CTimer _saveTimer;
/// <summary>
/// When true, the IncludedExcludedKeys dict will contain keys to include.
/// When false (default), IncludedExcludedKeys will contain keys to exclude.
/// </summary>
private static bool _excludeAllMode;
//static bool ExcludeNoKeyMessages;
private static readonly Dictionary<string, object> IncludedExcludedKeys;
private static readonly LoggerConfiguration _defaultLoggerConfiguration;
private static LoggerConfiguration _loggerConfiguration;
public static LoggerConfiguration LoggerConfiguration => _loggerConfiguration;
static Debug()
{
CrestronDataStoreStatic.InitCrestronDataStore();
var defaultConsoleLevel = GetStoredLogEventLevel(LevelStoreKey);
var defaultWebsocketLevel = GetStoredLogEventLevel(WebSocketLevelStoreKey);
var defaultErrorLogLevel = GetStoredLogEventLevel(ErrorLogLevelStoreKey);
var defaultFileLogLevel = GetStoredLogEventLevel(FileLevelStoreKey);
_consoleLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultConsoleLevel);
_websocketLoggingLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultWebsocketLevel);
_errorLogLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultErrorLogLevel);
_fileLevelSwitch = new LoggingLevelSwitch(initialMinimumLevel: defaultFileLogLevel);
_websocketSink = new DebugWebsocketSink(new JsonFormatter(renderMessage: true));
var logFilePath = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ?
$@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}app{InitialParametersClass.ApplicationNumber}{Path.DirectorySeparatorChar}global-log.log" :
$@"{Directory.GetApplicationRootDirectory()}{Path.DirectorySeparatorChar}user{Path.DirectorySeparatorChar}debug{Path.DirectorySeparatorChar}room{InitialParametersClass.RoomId}{Path.DirectorySeparatorChar}global-log.log";
CrestronConsole.PrintLine($"Saving log files to {logFilePath}");
var errorLogTemplate = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
? "{@t:fff}ms [{@l:u4}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}"
: "[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}";
_defaultLoggerConfiguration = new LoggerConfiguration()
.MinimumLevel.Verbose()
.Enrich.FromLogContext()
.Enrich.With(new CrestronEnricher())
.WriteTo.Sink(new DebugConsoleSink(new ExpressionTemplate("[{@t:yyyy-MM-dd HH:mm:ss.fff}][{@l:u4}][{App}]{#if Key is not null}[{Key}]{#end} {@m}{#if @x is not null}\r\n{@x}{#end}")), levelSwitch: _consoleLoggingLevelSwitch)
.WriteTo.Sink(_websocketSink, levelSwitch: _websocketLoggingLevelSwitch)
.WriteTo.Sink(new DebugErrorLogSink(new ExpressionTemplate(errorLogTemplate)), levelSwitch: _errorLogLevelSwitch)
.WriteTo.File(new RenderedCompactJsonFormatter(), logFilePath,
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: LogEventLevel.Debug,
retainedFileCountLimit: CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance ? 30 : 60,
levelSwitch: _fileLevelSwitch
);
try
{
if (InitialParametersClass.NumberOfRemovableDrives > 0)
{
CrestronConsole.PrintLine("{0} RM Drive(s) Present. Initializing CrestronLogger", InitialParametersClass.NumberOfRemovableDrives);
_defaultLoggerConfiguration.WriteTo.Sink(new DebugCrestronLoggerSink());
}
else
CrestronConsole.PrintLine("No RM Drive(s) Present. Not using Crestron Logger");
}
catch (Exception e)
{
CrestronConsole.PrintLine("Initializing of CrestronLogger failed: {0}", e);
}
// Instantiate the root logger
_loggerConfiguration = _defaultLoggerConfiguration;
_logger = _loggerConfiguration.CreateLogger();
// Get the assembly version and print it to console and the log
GetVersion();
string msg = $"[App {InitialParametersClass.ApplicationNumber}] Using PepperDash_Core v{PepperDashCoreVersion}";
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Server)
{
msg = $"[Room {InitialParametersClass.RoomId}] Using PepperDash_Core v{PepperDashCoreVersion}";
}
CrestronConsole.PrintLine(msg);
LogMessage(LogEventLevel.Information,msg);
IncludedExcludedKeys = new Dictionary<string, object>();
if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro)
{
// Add command to console
CrestronConsole.AddNewConsoleCommand(SetDoNotLoadOnNextBootFromConsole, "donotloadonnextboot",
"donotloadonnextboot:P [true/false]: Should the application load on next boot", ConsoleAccessLevelEnum.AccessOperator);
CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug",
"appdebug:P [0-5]: Sets the application's console debug message level",
ConsoleAccessLevelEnum.AccessOperator);
CrestronConsole.AddNewConsoleCommand(ShowDebugLog, "appdebuglog",
"appdebuglog:P [all] Use \"all\" for full log.",
ConsoleAccessLevelEnum.AccessOperator);
CrestronConsole.AddNewConsoleCommand(s => CrestronLogger.Clear(false), "appdebugclear",
"appdebugclear:P Clears the current custom log",
ConsoleAccessLevelEnum.AccessOperator);
CrestronConsole.AddNewConsoleCommand(SetDebugFilterFromConsole, "appdebugfilter",
"appdebugfilter [params]", ConsoleAccessLevelEnum.AccessOperator);
}
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
LoadMemory();
var context = _contexts.GetOrCreateItem("DEFAULT");
Level = context.Level;
DoNotLoadConfigOnNextBoot = context.DoNotLoadOnNextBoot;
if(DoNotLoadConfigOnNextBoot)
CrestronConsole.PrintLine(string.Format("Program {0} will not load config after next boot. Use console command go:{0} to load the config manually", InitialParametersClass.ApplicationNumber));
_consoleLoggingLevelSwitch.MinimumLevelChanged += (sender, args) =>
{
Console(0, "Console debug level set to {0}", _consoleLoggingLevelSwitch.MinimumLevel);
};
}
public static void UpdateLoggerConfiguration(LoggerConfiguration config)
{
_loggerConfiguration = config;
_logger = config.CreateLogger();
}
public static void ResetLoggerConfiguration()
{
_loggerConfiguration = _defaultLoggerConfiguration;
_logger = _loggerConfiguration.CreateLogger();
}
private static LogEventLevel GetStoredLogEventLevel(string levelStoreKey)
{
try
{
var result = CrestronDataStoreStatic.GetLocalIntValue(levelStoreKey, out int logLevel);
if (result != CrestronDataStore.CDS_ERROR.CDS_SUCCESS)
{
CrestronConsole.Print($"Unable to retrieve stored log level for {levelStoreKey}.\r\nError: {result}.\r\nSetting level to {LogEventLevel.Information}\r\n");
return LogEventLevel.Information;
}
if(logLevel < 0 || logLevel > 5)
{
CrestronConsole.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}");
return LogEventLevel.Information;
}
return (LogEventLevel)logLevel;
} catch (Exception ex)
{
CrestronConsole.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}");
return LogEventLevel.Information;
}
}
private static void GetVersion()
{
var assembly = Assembly.GetExecutingAssembly();
var ver =
assembly
.GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), false);
if (ver != null && ver.Length > 0)
{
if (ver[0] is AssemblyInformationalVersionAttribute verAttribute)
{
PepperDashCoreVersion = verAttribute.InformationalVersion;
}
}
else
{
var version = assembly.GetName().Version;
PepperDashCoreVersion = version.ToString();
}
}
/// <summary>
/// Used to save memory when shutting down
/// </summary>
/// <param name="programEventType"></param>
static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
Log.CloseAndFlush();
if (_saveTimer != null)
{
_saveTimer.Stop();
_saveTimer = null;
}
Console(0, "Saving debug settings");
SaveMemory();
}
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="levelString"></param>
public static void SetDebugFromConsole(string levelString)
{
try
{
if (levelString.Trim() == "?")
{
CrestronConsole.ConsoleCommandResponse(
$@"Used to set the minimum level of debug messages to be printed to the console:
{_logLevels[0]} = 0
{_logLevels[1]} = 1
{_logLevels[2]} = 2
{_logLevels[3]} = 3
{_logLevels[4]} = 4
{_logLevels[5]} = 5");
return;
}
if (string.IsNullOrEmpty(levelString.Trim()))
{
CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", _consoleLoggingLevelSwitch.MinimumLevel);
return;
}
if(int.TryParse(levelString, out var levelInt))
{
if(levelInt < 0 || levelInt > 5)
{
CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level. If using a number, value must be between 0-5");
return;
}
SetDebugLevel((uint) levelInt);
return;
}
if(Enum.TryParse<LogEventLevel>(levelString, out var levelEnum))
{
SetDebugLevel(levelEnum);
return;
}
CrestronConsole.ConsoleCommandResponse($"Error: Unable to parse {levelString} to valid log level");
}
catch
{
CrestronConsole.ConsoleCommandResponse("Usage: appdebug:P [0-5]");
}
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"> Valid values 0-5</param>
public static void SetDebugLevel(uint level)
{
if(!_logLevels.TryGetValue(level, out var logLevel))
{
logLevel = LogEventLevel.Information;
CrestronConsole.ConsoleCommandResponse($"{level} not valid. Setting level to {logLevel}");
SetDebugLevel(logLevel);
}
SetDebugLevel(logLevel);
}
public static void SetDebugLevel(LogEventLevel level)
{
_consoleLoggingLevelSwitch.MinimumLevel = level;
CrestronConsole.ConsoleCommandResponse("[Application {0}], Debug level set to {1}\r\n",
InitialParametersClass.ApplicationNumber, _consoleLoggingLevelSwitch.MinimumLevel);
CrestronConsole.ConsoleCommandResponse($"Storing level {level}:{(int) level}");
var err = CrestronDataStoreStatic.SetLocalIntValue(LevelStoreKey, (int) level);
CrestronConsole.ConsoleCommandResponse($"Store result: {err}:{(int)level}");
if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS)
CrestronConsole.PrintLine($"Error saving console debug level setting: {err}");
}
public static void SetWebSocketMinimumDebugLevel(LogEventLevel level)
{
_websocketLoggingLevelSwitch.MinimumLevel = level;
var err = CrestronDataStoreStatic.SetLocalUintValue(WebSocketLevelStoreKey, (uint) level);
if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS)
LogMessage(LogEventLevel.Information, "Error saving websocket debug level setting: {erro}", err);
LogMessage(LogEventLevel.Information, "Websocket debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel);
}
public static void SetErrorLogMinimumDebugLevel(LogEventLevel level)
{
_errorLogLevelSwitch.MinimumLevel = level;
var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level);
if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS)
LogMessage(LogEventLevel.Information, "Error saving Error Log debug level setting: {error}", err);
LogMessage(LogEventLevel.Information, "Error log debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel);
}
public static void SetFileMinimumDebugLevel(LogEventLevel level)
{
_errorLogLevelSwitch.MinimumLevel = level;
var err = CrestronDataStoreStatic.SetLocalUintValue(ErrorLogLevelStoreKey, (uint)level);
if (err != CrestronDataStore.CDS_ERROR.CDS_SUCCESS)
LogMessage(LogEventLevel.Information, "Error saving File debug level setting: {error}", err);
LogMessage(LogEventLevel.Information, "File debug level set to {0}", _websocketLoggingLevelSwitch.MinimumLevel);
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="stateString"></param>
public static void SetDoNotLoadOnNextBootFromConsole(string stateString)
{
try
{
if (string.IsNullOrEmpty(stateString.Trim()))
{
CrestronConsole.ConsoleCommandResponse("DoNotLoadOnNextBoot = {0}", DoNotLoadConfigOnNextBoot);
return;
}
SetDoNotLoadConfigOnNextBoot(bool.Parse(stateString));
}
catch
{
CrestronConsole.ConsoleCommandResponse("Usage: donotloadonnextboot:P [true/false]");
}
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="items"></param>
public static void SetDebugFilterFromConsole(string items)
{
var str = items.Trim();
if (str == "?")
{
CrestronConsole.ConsoleCommandResponse("Usage:\r APPDEBUGFILTER key1 key2 key3....\r " +
"+all: at beginning puts filter into 'default include' mode\r" +
" All keys that follow will be excluded from output.\r" +
"-all: at beginning puts filter into 'default exclude all' mode.\r" +
" All keys that follow will be the only keys that are shown\r" +
"+nokey: Enables messages with no key (default)\r" +
"-nokey: Disables messages with no key.\r" +
"(nokey settings are independent of all other settings)");
return;
}
var keys = Regex.Split(str, @"\s*");
foreach (var keyToken in keys)
{
var lkey = keyToken.ToLower();
if (lkey == "+all")
{
IncludedExcludedKeys.Clear();
_excludeAllMode = false;
}
else if (lkey == "-all")
{
IncludedExcludedKeys.Clear();
_excludeAllMode = true;
}
//else if (lkey == "+nokey")
//{
// ExcludeNoKeyMessages = false;
//}
//else if (lkey == "-nokey")
//{
// ExcludeNoKeyMessages = true;
//}
else
{
string key;
if (lkey.StartsWith("-"))
{
key = lkey.Substring(1);
// if in exclude all mode, we need to remove this from the inclusions
if (_excludeAllMode)
{
if (IncludedExcludedKeys.ContainsKey(key))
IncludedExcludedKeys.Remove(key);
}
// otherwise include all mode, add to the exclusions
else
{
IncludedExcludedKeys[key] = new object();
}
}
else if (lkey.StartsWith("+"))
{
key = lkey.Substring(1);
// if in exclude all mode, we need to add this as inclusion
if (_excludeAllMode)
{
IncludedExcludedKeys[key] = new object();
}
// otherwise include all mode, remove this from exclusions
else
{
if (IncludedExcludedKeys.ContainsKey(key))
IncludedExcludedKeys.Remove(key);
}
}
}
}
}
/// <summary>
/// sets the settings for a device or creates a new entry
/// </summary>
/// <param name="deviceKey"></param>
/// <param name="settings"></param>
/// <returns></returns>
public static void SetDeviceDebugSettings(string deviceKey, object settings)
{
_contexts.SetDebugSettingsForKey(deviceKey, settings);
SaveMemoryOnTimeout();
}
/// <summary>
/// Gets the device settings for a device by key or returns null
/// </summary>
/// <param name="deviceKey"></param>
/// <returns></returns>
public static object GetDeviceDebugSettingsForKey(string deviceKey)
{
return _contexts.GetDebugSettingsForKey(deviceKey);
}
/// <summary>
/// Sets the flag to prevent application starting on next boot
/// </summary>
/// <param name="state"></param>
public static void SetDoNotLoadConfigOnNextBoot(bool state)
{
DoNotLoadConfigOnNextBoot = state;
_contexts.GetOrCreateItem("DEFAULT").DoNotLoadOnNextBoot = state;
SaveMemoryOnTimeout();
CrestronConsole.ConsoleCommandResponse("[Application {0}], Do Not Load Config on Next Boot set to {1}",
InitialParametersClass.ApplicationNumber, DoNotLoadConfigOnNextBoot);
}
/// <summary>
///
/// </summary>
public static void ShowDebugLog(string s)
{
var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all");
foreach (var l in loglist)
CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine);
}
/// <summary>
/// Log an Exception using Serilog's default Exception logging mechanism
/// </summary>
/// <param name="ex">Exception to log</param>
/// <param name="message">Message template</param>
/// <param name="device">Optional IKeyed device. If provided, the Key of the device will be added to the log message</param>
/// <param name="args">Args to put into message template</param>
public static void LogMessage(Exception ex, string message, IKeyed device = null, params object[] args)
{
using (LogContext.PushProperty("Key", device?.Key))
{
_logger.Error(ex, message, args);
}
}
/// <summary>
/// Log a message
/// </summary>
/// <param name="level">Level to log at</param>
/// <param name="message">Message template</param>
/// <param name="device">Optional IKeyed device. If provided, the Key of the device will be added to the log message</param>
/// <param name="args">Args to put into message template</param>
public static void LogMessage(LogEventLevel level, string message, IKeyed device=null, params object[] args)
{
using (LogContext.PushProperty("Key", device?.Key))
{
_logger.Write(level, message, args);
}
}
public static void LogMessage(LogEventLevel level, string message, params object[] args)
{
LogMessage(level, message, null, args);
}
public static void LogMessage(LogEventLevel level, IKeyed keyed, string message, params object[] args)
{
LogMessage(level, message, keyed, args);
}
private static void LogMessage(uint level, string format, params object[] items)
{
if (!_logLevels.ContainsKey(level)) return;
var logLevel = _logLevels[level];
LogMessage(logLevel, format, items);
}
private static void LogMessage(uint level, IKeyed keyed, string format, params object[] items)
{
if (!_logLevels.ContainsKey(level)) return;
var logLevel = _logLevels[level];
LogMessage(logLevel, keyed, format, items);
}
/// <summary>
/// Prints message to console if current debug level is equal to or higher than the level of this message.
/// Uses CrestronConsole.PrintLine.
/// </summary>
/// <param name="level"></param>
/// <param name="format">Console format string</param>
/// <param name="items">Object parameters</param>
[Obsolete("Use LogMessage methods")]
public static void Console(uint level, string format, params object[] items)
{
LogMessage(level, format, items);
//if (IsRunningOnAppliance)
//{
// CrestronConsole.PrintLine("[{0}]App {1} Lvl {2}:{3}", DateTime.Now.ToString("HH:mm:ss.fff"),
// InitialParametersClass.ApplicationNumber,
// level,
// string.Format(format, items));
//}
}
/// <summary>
/// Logs to Console when at-level, and all messages to error log, including device key
/// </summary>
[Obsolete("Use LogMessage methods")]
public static void Console(uint level, IKeyed dev, string format, params object[] items)
{
LogMessage(level, dev, format, items);
//if (Level >= level)
// Console(level, "[{0}] {1}", dev.Key, message);
}
/// <summary>
/// Prints message to console if current debug level is equal to or higher than the level of this message. Always sends message to Error Log.
/// Uses CrestronConsole.PrintLine.
/// </summary>
[Obsolete("Use LogMessage methods")]
public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
LogMessage(level, dev, format, items);
}
/// <summary>
/// Logs to Console when at-level, and all messages to error log
/// </summary>
[Obsolete("Use LogMessage methods")]
public static void Console(uint level, ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
LogMessage(level, format, items);
}
/// <summary>
/// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at
/// or above the level provided, then the output will be written to both console and the log. Otherwise
/// it will only be written to the log.
/// </summary>
[Obsolete("Use LogMessage methods")]
public static void ConsoleWithLog(uint level, string format, params object[] items)
{
LogMessage(level, format, items);
// var str = string.Format(format, items);
//if (Level >= level)
// CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber, str);
// CrestronLogger.WriteToLog(str, level);
}
/// <summary>
/// Logs to both console and the custom user log (not the built-in error log). If appdebug level is set at
/// or above the level provided, then the output will be written to both console and the log. Otherwise
/// it will only be written to the log.
/// </summary>
[Obsolete("Use LogMessage methods")]
public static void ConsoleWithLog(uint level, IKeyed dev, string format, params object[] items)
{
LogMessage(level, dev, format, items);
// var str = string.Format(format, items);
// CrestronLogger.WriteToLog(string.Format("[{0}] {1}", dev.Key, str), level);
}
/// <summary>
/// Prints to log and error log
/// </summary>
/// <param name="errorLogLevel"></param>
/// <param name="str"></param>
[Obsolete("Use LogMessage methods")]
public static void LogError(ErrorLogLevel errorLogLevel, string str)
{
switch (errorLogLevel)
{
case ErrorLogLevel.Error:
LogMessage(LogEventLevel.Error, str);
break;
case ErrorLogLevel.Warning:
LogMessage(LogEventLevel.Warning, str);
break;
case ErrorLogLevel.Notice:
LogMessage(LogEventLevel.Information, str);
break;
}
}
/// <summary>
/// Writes the memory object after timeout
/// </summary>
static void SaveMemoryOnTimeout()
{
Console(0, "Saving debug settings");
if (_saveTimer == null)
_saveTimer = new CTimer(o =>
{
_saveTimer = null;
SaveMemory();
}, SaveTimeoutMs);
else
_saveTimer.Reset(SaveTimeoutMs);
}
/// <summary>
/// Writes the memory - use SaveMemoryOnTimeout
/// </summary>
static void SaveMemory()
{
//var dir = @"\NVRAM\debug";
//if (!Directory.Exists(dir))
// Directory.Create(dir);
var fileName = GetMemoryFileName();
Console(0, ErrorLogLevel.Notice, "Loading debug settings file from {0}", fileName);
using (var sw = new StreamWriter(fileName))
{
var json = JsonConvert.SerializeObject(_contexts);
sw.Write(json);
sw.Flush();
}
}
/// <summary>
///
/// </summary>
static void LoadMemory()
{
var file = GetMemoryFileName();
if (File.Exists(file))
{
using (var sr = new StreamReader(file))
{
var json = sr.ReadToEnd();
_contexts = JsonConvert.DeserializeObject<DebugContextCollection>(json);
if (_contexts != null)
{
Console(1, "Debug memory restored from file");
return;
}
}
}
_contexts = new DebugContextCollection();
}
/// <summary>
/// Helper to get the file path for this app's debug memory
/// </summary>
static string GetMemoryFileName()
{
if (CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance)
{
// CheckForMigration();
return string.Format(@"\user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber);
}
return string.Format("{0}{1}user{1}debugSettings{1}{2}.json",Directory.GetApplicationRootDirectory(), Path.DirectorySeparatorChar, InitialParametersClass.RoomId);
}
private static void CheckForMigration()
{
var oldFilePath = String.Format(@"\nvram\debugSettings\program{0}", InitialParametersClass.ApplicationNumber);
var newFilePath = String.Format(@"\user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber);
//check for file at old path
if (!File.Exists(oldFilePath))
{
Console(0, ErrorLogLevel.Notice,
String.Format(
@"Debug settings file migration not necessary. Using file at \user\debugSettings\program{0}",
InitialParametersClass.ApplicationNumber));
return;
}
//create the new directory if it doesn't already exist
if (!Directory.Exists(@"\user\debugSettings"))
{
Directory.CreateDirectory(@"\user\debugSettings");
}
Console(0, ErrorLogLevel.Notice,
String.Format(
@"File found at \nvram\debugSettings\program{0}. Migrating to \user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber));
//Copy file from old path to new path, then delete it. This will overwrite the existing file
File.Copy(oldFilePath, newFilePath, true);
File.Delete(oldFilePath);
//Check if the old directory is empty, then delete it if it is
if (Directory.GetFiles(@"\nvram\debugSettings").Length > 0)
{
return;
}
Directory.Delete(@"\nvram\debugSettings");
}
/// <summary>
/// Error level to for message to be logged at
/// </summary>
public enum ErrorLogLevel
{
/// <summary>
/// Error
/// </summary>
Error,
/// <summary>
/// Warning
/// </summary>
Warning,
/// <summary>
/// Notice
/// </summary>
Notice,
/// <summary>
/// None
/// </summary>
None,
}
}
}

View File

@@ -0,0 +1,55 @@
using Crestron.SimplSharp;
using Serilog.Configuration;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Json;
using System.IO;
using System.Text;
namespace PepperDash.Core
{
public class DebugConsoleSink : ILogEventSink
{
private readonly ITextFormatter _textFormatter;
public void Emit(LogEvent logEvent)
{
if (!Debug.IsRunningOnAppliance) return;
/*string message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}]{logEvent.RenderMessage()}";
if(logEvent.Properties.TryGetValue("Key",out var value) && value is ScalarValue sv && sv.Value is string rawValue)
{
message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}][{rawValue,3}]: {logEvent.RenderMessage()}";
}*/
var buffer = new StringWriter(new StringBuilder(256));
_textFormatter.Format(logEvent, buffer);
var message = buffer.ToString();
CrestronConsole.PrintLine(message);
}
public DebugConsoleSink(ITextFormatter formatProvider )
{
_textFormatter = formatProvider ?? new JsonFormatter();
}
}
public static class DebugConsoleSinkExtensions
{
public static LoggerConfiguration DebugConsoleSink(
this LoggerSinkConfiguration loggerConfiguration,
ITextFormatter formatProvider = null)
{
return loggerConfiguration.Sink(new DebugConsoleSink(formatProvider));
}
}
}

View File

@@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronIO;
using Newtonsoft.Json;
namespace PepperDash.Core
{
/// <summary>
/// Represents a debugging context
/// </summary>
public class DebugContext
{
/// <summary>
/// Describes the folder location where a given program stores it's debug level memory. By default, the
/// file written will be named appNdebug where N is 1-10.
/// </summary>
public string Key { get; private set; }
///// <summary>
///// The name of the file containing the current debug settings.
///// </summary>
//string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber);
DebugContextSaveData SaveData;
int SaveTimeoutMs = 30000;
CTimer SaveTimer;
static List<DebugContext> Contexts = new List<DebugContext>();
/// <summary>
/// Creates or gets a debug context
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public static DebugContext GetDebugContext(string key)
{
var context = Contexts.FirstOrDefault(c => c.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (context == null)
{
context = new DebugContext(key);
Contexts.Add(context);
}
return context;
}
/// <summary>
/// Do not use. For S+ access.
/// </summary>
public DebugContext() { }
DebugContext(string key)
{
Key = key;
if (CrestronEnvironment.RuntimeEnvironment == eRuntimeEnvironment.SimplSharpPro)
{
// Add command to console
CrestronConsole.AddNewConsoleCommand(SetDebugFromConsole, "appdebug",
"appdebug:P [0-2]: Sets the application's console debug message level",
ConsoleAccessLevelEnum.AccessOperator);
}
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
LoadMemory();
}
/// <summary>
/// Used to save memory when shutting down
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType == eProgramStatusEventType.Stopping)
{
if (SaveTimer != null)
{
SaveTimer.Stop();
SaveTimer = null;
}
Console(0, "Saving debug settings");
SaveMemory();
}
}
/// <summary>
/// Callback for console command
/// </summary>
/// <param name="levelString"></param>
public void SetDebugFromConsole(string levelString)
{
try
{
if (string.IsNullOrEmpty(levelString.Trim()))
{
CrestronConsole.ConsoleCommandResponse("AppDebug level = {0}", SaveData.Level);
return;
}
SetDebugLevel(Convert.ToInt32(levelString));
}
catch
{
CrestronConsole.PrintLine("Usage: appdebug:P [0-2]");
}
}
/// <summary>
/// Sets the debug level
/// </summary>
/// <param name="level"> Valid values 0 (no debug), 1 (critical), 2 (all messages)</param>
public void SetDebugLevel(int level)
{
if (level <= 2)
{
SaveData.Level = level;
SaveMemoryOnTimeout();
CrestronConsole.PrintLine("[Application {0}], Debug level set to {1}",
InitialParametersClass.ApplicationNumber, SaveData.Level);
}
}
/// <summary>
/// Prints message to console if current debug level is equal to or higher than the level of this message.
/// Uses CrestronConsole.PrintLine.
/// </summary>
/// <param name="level"></param>
/// <param name="format">Console format string</param>
/// <param name="items">Object parameters</param>
public void Console(uint level, string format, params object[] items)
{
if (SaveData.Level >= level)
CrestronConsole.PrintLine("App {0}:{1}", InitialParametersClass.ApplicationNumber,
string.Format(format, items));
}
/// <summary>
/// Appends a device Key to the beginning of a message
/// </summary>
public void Console(uint level, IKeyed dev, string format, params object[] items)
{
if (SaveData.Level >= level)
Console(level, "[{0}] {1}", dev.Key, string.Format(format, items));
}
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <param name="dev"></param>
/// <param name="errorLogLevel"></param>
/// <param name="format"></param>
/// <param name="items"></param>
public void Console(uint level, IKeyed dev, Debug.ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
if (SaveData.Level >= level)
{
var str = string.Format("[{0}] {1}", dev.Key, string.Format(format, items));
Console(level, str);
LogError(errorLogLevel, str);
}
}
/// <summary>
///
/// </summary>
/// <param name="level"></param>
/// <param name="errorLogLevel"></param>
/// <param name="format"></param>
/// <param name="items"></param>
public void Console(uint level, Debug.ErrorLogLevel errorLogLevel,
string format, params object[] items)
{
if (SaveData.Level >= level)
{
var str = string.Format(format, items);
Console(level, str);
LogError(errorLogLevel, str);
}
}
/// <summary>
///
/// </summary>
/// <param name="errorLogLevel"></param>
/// <param name="str"></param>
public void LogError(Debug.ErrorLogLevel errorLogLevel, string str)
{
string msg = string.Format("App {0}:{1}", InitialParametersClass.ApplicationNumber, str);
switch (errorLogLevel)
{
case Debug.ErrorLogLevel.Error:
ErrorLog.Error(msg);
break;
case Debug.ErrorLogLevel.Warning:
ErrorLog.Warn(msg);
break;
case Debug.ErrorLogLevel.Notice:
ErrorLog.Notice(msg);
break;
}
}
/// <summary>
/// Writes the memory object after timeout
/// </summary>
void SaveMemoryOnTimeout()
{
if (SaveTimer == null)
SaveTimer = new CTimer(o =>
{
SaveTimer = null;
SaveMemory();
}, SaveTimeoutMs);
else
SaveTimer.Reset(SaveTimeoutMs);
}
/// <summary>
/// Writes the memory - use SaveMemoryOnTimeout
/// </summary>
void SaveMemory()
{
using (StreamWriter sw = new StreamWriter(GetMemoryFileName()))
{
var json = JsonConvert.SerializeObject(SaveData);
sw.Write(json);
sw.Flush();
}
}
/// <summary>
///
/// </summary>
void LoadMemory()
{
var file = GetMemoryFileName();
if (File.Exists(file))
{
using (StreamReader sr = new StreamReader(file))
{
var data = JsonConvert.DeserializeObject<DebugContextSaveData>(sr.ReadToEnd());
if (data != null)
{
SaveData = data;
Debug.Console(1, "Debug memory restored from file");
return;
}
else
SaveData = new DebugContextSaveData();
}
}
}
/// <summary>
/// Helper to get the file path for this app's debug memory
/// </summary>
string GetMemoryFileName()
{
return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key);
}
}
/// <summary>
///
/// </summary>
public class DebugContextSaveData
{
/// <summary>
///
/// </summary>
public int Level { get; set; }
}
}

View File

@@ -0,0 +1,29 @@
using Crestron.SimplSharp;
using Crestron.SimplSharp.CrestronLogger;
using Serilog.Core;
using Serilog.Events;
namespace PepperDash.Core.Logging
{
public class DebugCrestronLoggerSink : ILogEventSink
{
public void Emit(LogEvent logEvent)
{
if (!Debug.IsRunningOnAppliance) return;
string message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}]{logEvent.RenderMessage()}";
if (logEvent.Properties.TryGetValue("Key", out var value) && value is ScalarValue sv && sv.Value is string rawValue)
{
message = $"[{logEvent.Timestamp}][{logEvent.Level}][App {InitialParametersClass.ApplicationNumber}][{rawValue}]: {logEvent.RenderMessage()}";
}
CrestronLogger.WriteToLog(message, (uint)logEvent.Level);
}
public DebugCrestronLoggerSink()
{
CrestronLogger.Initialize(1, LoggerModeEnum.RM);
}
}
}

View File

@@ -0,0 +1,65 @@
using Crestron.SimplSharp;
using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PepperDash.Core.Logging
{
public class DebugErrorLogSink : ILogEventSink
{
private ITextFormatter _formatter;
private Dictionary<LogEventLevel, Action<string>> _errorLogMap = new Dictionary<LogEventLevel, Action<string>>
{
{ LogEventLevel.Verbose, (msg) => ErrorLog.Notice(msg) },
{LogEventLevel.Debug, (msg) => ErrorLog.Notice(msg) },
{LogEventLevel.Information, (msg) => ErrorLog.Notice(msg) },
{LogEventLevel.Warning, (msg) => ErrorLog.Warn(msg) },
{LogEventLevel.Error, (msg) => ErrorLog.Error(msg) },
{LogEventLevel.Fatal, (msg) => ErrorLog.Error(msg) }
};
public void Emit(LogEvent logEvent)
{
string message;
if (_formatter == null)
{
var programId = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance
? $"App {InitialParametersClass.ApplicationNumber}"
: $"Room {InitialParametersClass.RoomId}";
message = $"[{logEvent.Timestamp}][{logEvent.Level}][{programId}]{logEvent.RenderMessage()}";
if (logEvent.Properties.TryGetValue("Key", out var value) && value is ScalarValue sv && sv.Value is string rawValue)
{
message = $"[{logEvent.Timestamp}][{logEvent.Level}][{programId}][{rawValue}]: {logEvent.RenderMessage()}";
}
} else
{
var buffer = new StringWriter(new StringBuilder(256));
_formatter.Format(logEvent, buffer);
message = buffer.ToString();
}
if(!_errorLogMap.TryGetValue(logEvent.Level, out var handler))
{
return;
}
handler(message);
}
public DebugErrorLogSink(ITextFormatter formatter = null)
{
_formatter = formatter;
}
}
}

View File

@@ -0,0 +1,44 @@
using Serilog;
using Serilog.Events;
using System;
using Log = PepperDash.Core.Debug;
namespace PepperDash.Core.Logging
{
public static class DebugExtensions
{
public static void LogException(this IKeyed device, Exception ex, string message, params object[] args)
{
Log.LogMessage(ex, message, device, args);
}
public static void LogVerbose(this IKeyed device, string message, params object[] args)
{
Log.LogMessage(LogEventLevel.Verbose, device, message, args);
}
public static void LogDebug(this IKeyed device, string message, params object[] args)
{
Log.LogMessage(LogEventLevel.Debug, device, message, args);
}
public static void LogInformation(this IKeyed device, string message, params object[] args)
{
Log.LogMessage(LogEventLevel.Information, device, message, args);
}
public static void LogWarning(this IKeyed device, string message, params object[] args)
{
Log.LogMessage(LogEventLevel.Warning, device, message, args);
}
public static void LogError(this IKeyed device, string message, params object[] args)
{
Log.LogMessage(LogEventLevel.Error, device, message, args);
}
public static void LogFatal(this IKeyed device, string message, params object[] args)
{
Log.LogMessage(LogEventLevel.Fatal, device, message, args);
}
}
}

View File

@@ -0,0 +1,115 @@
using System.Collections.Generic;
using Crestron.SimplSharp;
using Newtonsoft.Json;
namespace PepperDash.Core.Logging
{
/// <summary>
/// Class to persist current Debug settings across program restarts
/// </summary>
public class DebugContextCollection
{
/// <summary>
/// To prevent threading issues with the DeviceDebugSettings collection
/// </summary>
private readonly CCriticalSection _deviceDebugSettingsLock;
[JsonProperty("items")] private readonly Dictionary<string, DebugContextItem> _items;
/// <summary>
/// Collection of the debug settings for each device where the dictionary key is the device key
/// </summary>
[JsonProperty("deviceDebugSettings")]
private Dictionary<string, object> DeviceDebugSettings { get; set; }
/// <summary>
/// Default constructor
/// </summary>
public DebugContextCollection()
{
_deviceDebugSettingsLock = new CCriticalSection();
DeviceDebugSettings = new Dictionary<string, object>();
_items = new Dictionary<string, DebugContextItem>();
}
/// <summary>
/// Sets the level of a given context item, and adds that item if it does not
/// exist
/// </summary>
/// <param name="contextKey"></param>
/// <param name="level"></param>
public void SetLevel(string contextKey, int level)
{
if (level < 0 || level > 2)
return;
GetOrCreateItem(contextKey).Level = level;
}
/// <summary>
/// Gets a level or creates it if not existing
/// </summary>
/// <param name="contextKey"></param>
/// <returns></returns>
public DebugContextItem GetOrCreateItem(string contextKey)
{
if (!_items.ContainsKey(contextKey))
_items[contextKey] = new DebugContextItem { Level = 0 };
return _items[contextKey];
}
/// <summary>
/// sets the settings for a device or creates a new entry
/// </summary>
/// <param name="deviceKey"></param>
/// <param name="settings"></param>
/// <returns></returns>
public void SetDebugSettingsForKey(string deviceKey, object settings)
{
try
{
_deviceDebugSettingsLock.Enter();
if (DeviceDebugSettings.ContainsKey(deviceKey))
{
DeviceDebugSettings[deviceKey] = settings;
}
else
DeviceDebugSettings.Add(deviceKey, settings);
}
finally
{
_deviceDebugSettingsLock.Leave();
}
}
/// <summary>
/// Gets the device settings for a device by key or returns null
/// </summary>
/// <param name="deviceKey"></param>
/// <returns></returns>
public object GetDebugSettingsForKey(string deviceKey)
{
return DeviceDebugSettings[deviceKey];
}
}
/// <summary>
/// Contains information about
/// </summary>
public class DebugContextItem
{
/// <summary>
/// The level of debug messages to print
/// </summary>
[JsonProperty("level")]
public int Level { get; set; }
/// <summary>
/// Property to tell the program not to intitialize when it boots, if desired
/// </summary>
[JsonProperty("doNotLoadOnNextBoot")]
public bool DoNotLoadOnNextBoot { get; set; }
}
}

View File

@@ -0,0 +1,271 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Serilog.Configuration;
using WebSocketSharp.Server;
using Crestron.SimplSharp;
using WebSocketSharp;
using System.Security.Authentication;
using WebSocketSharp.Net;
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
using System.IO;
using Org.BouncyCastle.Asn1.X509;
using Serilog.Formatting;
using Newtonsoft.Json.Linq;
using Serilog.Formatting.Json;
namespace PepperDash.Core
{
public class DebugWebsocketSink : ILogEventSink
{
private HttpServer _httpsServer;
private string _path = "/debug/join/";
private const string _certificateName = "selfCres";
private const string _certificatePassword = "cres12345";
public int Port
{ get
{
if(_httpsServer == null) return 0;
return _httpsServer.Port;
}
}
public string Url
{
get
{
if (_httpsServer == null) return "";
return $"wss://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{_httpsServer.Port}{_httpsServer.WebSocketServices[_path].Path}";
}
}
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
private readonly ITextFormatter _textFormatter;
public DebugWebsocketSink(ITextFormatter formatProvider)
{
_textFormatter = formatProvider ?? new JsonFormatter();
if (!File.Exists($"\\user\\{_certificateName}.pfx"))
CreateCert(null);
CrestronEnvironment.ProgramStatusEventHandler += type =>
{
if (type == eProgramStatusEventType.Stopping)
{
StopServer();
}
};
}
private void CreateCert(string[] args)
{
try
{
//Debug.Console(0,"CreateCert Creating Utility");
CrestronConsole.PrintLine("CreateCert Creating Utility");
//var utility = new CertificateUtility();
var utility = new BouncyCertificate();
//Debug.Console(0, "CreateCert Calling CreateCert");
CrestronConsole.PrintLine("CreateCert Calling CreateCert");
//utility.CreateCert();
var ipAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
var hostName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
var domainName = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, 0);
//Debug.Console(0, "DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress);
CrestronConsole.PrintLine(string.Format("DomainName: {0} | HostName: {1} | {1}.{0}@{2}", domainName, hostName, ipAddress));
var certificate = utility.CreateSelfSignedCertificate(string.Format("CN={0}.{1}", hostName, domainName), new[] { string.Format("{0}.{1}", hostName, domainName), ipAddress }, new[] { KeyPurposeID.id_kp_serverAuth, KeyPurposeID.id_kp_clientAuth });
//Crestron fails to let us do this...perhaps it should be done through their Dll's but haven't tested
//Debug.Print($"CreateCert Storing Certificate To My.LocalMachine");
//utility.AddCertToStore(certificate, StoreName.My, StoreLocation.LocalMachine);
//Debug.Console(0, "CreateCert Saving Cert to \\user\\");
CrestronConsole.PrintLine("CreateCert Saving Cert to \\user\\");
utility.CertificatePassword = _certificatePassword;
utility.WriteCertificate(certificate, @"\user\", _certificateName);
//Debug.Console(0, "CreateCert Ending CreateCert");
CrestronConsole.PrintLine("CreateCert Ending CreateCert");
}
catch (Exception ex)
{
//Debug.Console(0, "WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace);
CrestronConsole.PrintLine(string.Format("WSS CreateCert Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace));
}
}
public void Emit(LogEvent logEvent)
{
if (_httpsServer == null || !_httpsServer.IsListening) return;
var sw = new StringWriter();
_textFormatter.Format(logEvent, sw);
_httpsServer.WebSocketServices.Broadcast(sw.ToString());
}
public void StartServerAndSetPort(int port)
{
Debug.Console(0, "Starting Websocket Server on port: {0}", port);
Start(port, $"\\user\\{_certificateName}.pfx", _certificatePassword);
}
private void Start(int port, string certPath = "", string certPassword = "")
{
try
{
_httpsServer = new HttpServer(port, true);
if (!string.IsNullOrWhiteSpace(certPath))
{
Debug.Console(0, "Assigning SSL Configuration");
_httpsServer.SslConfiguration = new ServerSslConfiguration(new X509Certificate2(certPath, certPassword))
{
ClientCertificateRequired = false,
CheckCertificateRevocation = false,
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls,
//this is just to test, you might want to actually validate
ClientCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
{
Debug.Console(0, "HTTPS ClientCerticateValidation Callback triggered");
return true;
}
};
}
Debug.Console(0, "Adding Debug Client Service");
_httpsServer.AddWebSocketService<DebugClient>(_path);
Debug.Console(0, "Assigning Log Info");
_httpsServer.Log.Level = LogLevel.Trace;
_httpsServer.Log.Output = (d, s) =>
{
uint level;
switch(d.Level)
{
case WebSocketSharp.LogLevel.Fatal:
level = 3;
break;
case WebSocketSharp.LogLevel.Error:
level = 2;
break;
case WebSocketSharp.LogLevel.Warn:
level = 1;
break;
case WebSocketSharp.LogLevel.Info:
level = 0;
break;
case WebSocketSharp.LogLevel.Debug:
level = 4;
break;
case WebSocketSharp.LogLevel.Trace:
level = 5;
break;
default:
level = 4;
break;
}
Debug.Console(level, "{1} {0}\rCaller:{2}\rMessage:{3}\rs:{4}", d.Level.ToString(), d.Date.ToString(), d.Caller.ToString(), d.Message, s);
};
Debug.Console(0, "Starting");
_httpsServer.Start();
Debug.Console(0, "Ready");
}
catch (Exception ex)
{
Debug.Console(0, "WebSocket Failed to start {0}", ex.Message);
}
}
public void StopServer()
{
Debug.Console(0, "Stopping Websocket Server");
_httpsServer?.Stop();
_httpsServer = null;
}
}
public static class DebugWebsocketSinkExtensions
{
public static LoggerConfiguration DebugWebsocketSink(
this LoggerSinkConfiguration loggerConfiguration,
ITextFormatter formatProvider = null)
{
return loggerConfiguration.Sink(new DebugWebsocketSink(formatProvider));
}
}
public class DebugClient : WebSocketBehavior
{
private DateTime _connectionTime;
public TimeSpan ConnectedDuration
{
get
{
if (Context.WebSocket.IsAlive)
{
return DateTime.Now - _connectionTime;
}
else
{
return new TimeSpan(0);
}
}
}
public DebugClient()
{
Debug.Console(0, "DebugClient Created");
}
protected override void OnOpen()
{
base.OnOpen();
var url = Context.WebSocket.Url;
Debug.Console(0, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url);
_connectionTime = DateTime.Now;
}
protected override void OnMessage(MessageEventArgs e)
{
base.OnMessage(e);
Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data);
}
protected override void OnClose(CloseEventArgs e)
{
base.OnClose(e);
Debug.Console(0, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason);
}
protected override void OnError(WebSocketSharp.ErrorEventArgs e)
{
base.OnError(e);
Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message);
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core
{
/// <summary>
/// Not in use
/// </summary>
public static class NetworkComm
{
/// <summary>
/// Not in use
/// </summary>
static NetworkComm()
{
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.PasswordManagement
{
/// <summary>
/// JSON password configuration
/// </summary>
public class PasswordConfig
{
/// <summary>
/// Password object configured password
/// </summary>
public string password { get; set; }
/// <summary>
/// Constructor
/// </summary>
public PasswordConfig()
{
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.PasswordManagement
{
/// <summary>
/// Constants
/// </summary>
public class PasswordManagementConstants
{
/// <summary>
/// Generic boolean value change constant
/// </summary>
public const ushort BoolValueChange = 1;
/// <summary>
/// Evaluated boolean change constant
/// </summary>
public const ushort PasswordInitializedChange = 2;
/// <summary>
/// Update busy change const
/// </summary>
public const ushort PasswordUpdateBusyChange = 3;
/// <summary>
/// Password is valid change constant
/// </summary>
public const ushort PasswordValidationChange = 4;
/// <summary>
/// Password LED change constant
/// </summary>
public const ushort PasswordLedFeedbackChange = 5;
/// <summary>
/// Generic ushort value change constant
/// </summary>
public const ushort UshrtValueChange = 101;
/// <summary>
/// Password count
/// </summary>
public const ushort PasswordManagerCountChange = 102;
/// <summary>
/// Password selecte index change constant
/// </summary>
public const ushort PasswordSelectIndexChange = 103;
/// <summary>
/// Password length
/// </summary>
public const ushort PasswordLengthChange = 104;
/// <summary>
/// Generic string value change constant
/// </summary>
public const ushort StringValueChange = 201;
}
}

View File

@@ -0,0 +1,187 @@
using System;
namespace PepperDash.Core.PasswordManagement
{
/// <summary>
/// A class to allow user interaction with the PasswordManager
/// </summary>
public class PasswordClient
{
/// <summary>
/// Password selected
/// </summary>
public string Password { get; set; }
/// <summary>
/// Password selected key
/// </summary>
public ushort Key { get; set; }
/// <summary>
/// Used to build the password entered by the user
/// </summary>
public string PasswordToValidate { get; set; }
/// <summary>
/// Boolean event
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Ushort event
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
/// <summary>
/// String event
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Constructor
/// </summary>
public PasswordClient()
{
PasswordManager.PasswordChange += new EventHandler<StringChangeEventArgs>(PasswordManager_PasswordChange);
}
/// <summary>
/// Initialize method
/// </summary>
public void Initialize()
{
OnBoolChange(false, 0, PasswordManagementConstants.PasswordInitializedChange);
Password = "";
PasswordToValidate = "";
OnUshrtChange((ushort)PasswordManager.Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange);
OnBoolChange(true, 0, PasswordManagementConstants.PasswordInitializedChange);
}
/// <summary>
/// Retrieve password by index
/// </summary>
/// <param name="key"></param>
public void GetPasswordByIndex(ushort key)
{
OnUshrtChange((ushort)PasswordManager.Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange);
Key = key;
var pw = PasswordManager.Passwords[Key];
if (pw == null)
{
OnUshrtChange(0, 0, PasswordManagementConstants.PasswordLengthChange);
return;
}
Password = pw;
OnUshrtChange((ushort)Password.Length, 0, PasswordManagementConstants.PasswordLengthChange);
OnUshrtChange(key, 0, PasswordManagementConstants.PasswordSelectIndexChange);
}
/// <summary>
/// Password validation method
/// </summary>
/// <param name="password"></param>
public void ValidatePassword(string password)
{
if (string.IsNullOrEmpty(password))
return;
if (string.Equals(Password, password))
OnBoolChange(true, 0, PasswordManagementConstants.PasswordValidationChange);
else
OnBoolChange(false, 0, PasswordManagementConstants.PasswordValidationChange);
ClearPassword();
}
/// <summary>
/// Builds the user entered passwrod string, will attempt to validate the user entered
/// password against the selected password when the length of the 2 are equal
/// </summary>
/// <param name="data"></param>
public void BuildPassword(string data)
{
PasswordToValidate = String.Concat(PasswordToValidate, data);
OnBoolChange(true, (ushort)PasswordToValidate.Length, PasswordManagementConstants.PasswordLedFeedbackChange);
if (PasswordToValidate.Length == Password.Length)
ValidatePassword(PasswordToValidate);
}
/// <summary>
/// Clears the user entered password and resets the LEDs
/// </summary>
public void ClearPassword()
{
PasswordToValidate = "";
OnBoolChange(false, (ushort)PasswordToValidate.Length, PasswordManagementConstants.PasswordLedFeedbackChange);
}
/// <summary>
/// Protected boolean change event handler
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// Protected ushort change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUshrtChange(ushort value, ushort index, ushort type)
{
var handler = UshrtChange;
if (handler != null)
{
var args = new UshrtChangeEventArgs(value, type);
args.Index = index;
UshrtChange(this, args);
}
}
/// <summary>
/// Protected string change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
/// <summary>
/// If password changes while selected change event will be notifed and update the client
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
protected void PasswordManager_PasswordChange(object sender, StringChangeEventArgs args)
{
//throw new NotImplementedException();
if (Key == args.Index)
{
//PasswordSelectedKey = args.Index;
//PasswordSelected = args.StringValue;
GetPasswordByIndex(args.Index);
}
}
}
}

View File

@@ -0,0 +1,241 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp;
namespace PepperDash.Core.PasswordManagement
{
/// <summary>
/// Allows passwords to be stored and managed
/// </summary>
public class PasswordManager
{
/// <summary>
/// Public dictionary of known passwords
/// </summary>
public static Dictionary<uint, string> Passwords = new Dictionary<uint, string>();
/// <summary>
/// Private dictionary, used when passwords are updated
/// </summary>
private Dictionary<uint, string> _passwords = new Dictionary<uint, string>();
/// <summary>
/// Timer used to wait until password changes have stopped before updating the dictionary
/// </summary>
CTimer PasswordTimer;
/// <summary>
/// Timer length
/// </summary>
public long PasswordTimerElapsedMs = 5000;
/// <summary>
/// Boolean event
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Ushort event
/// </summary>
public event EventHandler<UshrtChangeEventArgs> UshrtChange;
/// <summary>
/// String event
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Event to notify clients of an updated password at the specified index (uint)
/// </summary>
public static event EventHandler<StringChangeEventArgs> PasswordChange;
/// <summary>
/// Constructor
/// </summary>
public PasswordManager()
{
}
/// <summary>
/// Initialize password manager
/// </summary>
public void Initialize()
{
if (Passwords == null)
Passwords = new Dictionary<uint, string>();
if (_passwords == null)
_passwords = new Dictionary<uint, string>();
OnBoolChange(true, 0, PasswordManagementConstants.PasswordInitializedChange);
}
/// <summary>
/// Updates password stored in the dictonary
/// </summary>
/// <param name="key"></param>
/// <param name="password"></param>
public void UpdatePassword(ushort key, string password)
{
// validate the parameters
if (key > 0 && string.IsNullOrEmpty(password))
{
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: key [{0}] or password are not valid", key, password));
return;
}
try
{
// if key exists, update the value
if(_passwords.ContainsKey(key))
_passwords[key] = password;
// else add the key & value
else
_passwords.Add(key, password);
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: _password[{0}] = {1}", key, _passwords[key]));
if (PasswordTimer == null)
{
PasswordTimer = new CTimer((o) => PasswordTimerElapsed(), PasswordTimerElapsedMs);
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Started"));
OnBoolChange(true, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
}
else
{
PasswordTimer.Reset(PasswordTimerElapsedMs);
Debug.Console(1, string.Format("PasswordManager.UpdatePassword: CTimer Reset"));
}
}
catch (Exception e)
{
var msg = string.Format("PasswordManager.UpdatePassword key-value[{0}, {1}] failed:\r{2}", key, password, e);
Debug.Console(1, msg);
}
}
/// <summary>
/// CTimer callback function
/// </summary>
private void PasswordTimerElapsed()
{
try
{
PasswordTimer.Stop();
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: CTimer Stopped"));
OnBoolChange(false, 0, PasswordManagementConstants.PasswordUpdateBusyChange);
foreach (var pw in _passwords)
{
// if key exists, continue
if (Passwords.ContainsKey(pw.Key))
{
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: pw.key[{0}] = {1}", pw.Key, pw.Value));
if (Passwords[pw.Key] != _passwords[pw.Key])
{
Passwords[pw.Key] = _passwords[pw.Key];
Debug.Console(1, string.Format("PasswordManager.PasswordTimerElapsed: Updated Password[{0} = {1}", pw.Key, Passwords[pw.Key]));
OnPasswordChange(Passwords[pw.Key], (ushort)pw.Key, PasswordManagementConstants.StringValueChange);
}
}
// else add the key & value
else
{
Passwords.Add(pw.Key, pw.Value);
}
}
OnUshrtChange((ushort)Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange);
}
catch (Exception e)
{
var msg = string.Format("PasswordManager.PasswordTimerElapsed failed:\r{0}", e);
Debug.Console(1, msg);
}
}
/// <summary>
/// Method to change the default timer value, (default 5000ms/5s)
/// </summary>
/// <param name="time"></param>
public void PasswordTimerMs(ushort time)
{
PasswordTimerElapsedMs = Convert.ToInt64(time);
}
/// <summary>
/// Helper method for debugging to see what passwords are in the lists
/// </summary>
public void ListPasswords()
{
Debug.Console(0, "PasswordManager.ListPasswords:\r");
foreach (var pw in Passwords)
Debug.Console(0, "Passwords[{0}]: {1}\r", pw.Key, pw.Value);
Debug.Console(0, "\n");
foreach (var pw in _passwords)
Debug.Console(0, "_passwords[{0}]: {1}\r", pw.Key, pw.Value);
}
/// <summary>
/// Protected boolean change event handler
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// Protected ushort change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnUshrtChange(ushort value, ushort index, ushort type)
{
var handler = UshrtChange;
if (handler != null)
{
var args = new UshrtChangeEventArgs(value, type);
args.Index = index;
UshrtChange(this, args);
}
}
/// <summary>
/// Protected string change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
/// <summary>
/// Protected password change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnPasswordChange(string value, ushort index, ushort type)
{
var handler = PasswordChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
PasswordChange(this, args);
}
}
}
}

View File

@@ -0,0 +1,66 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ProjectType>Library</ProjectType>
</PropertyGroup>
<PropertyGroup>
<RootNamespace>PepperDash.Core</RootNamespace>
<AssemblyName>PepperDashCore</AssemblyName>
<TargetFramework>net472</TargetFramework>
<Deterministic>true</Deterministic>
<NeutralLanguage>en</NeutralLanguage>
<OutputPath>bin\$(Configuration)\</OutputPath>
<SignAssembly>False</SignAssembly>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Title>PepperDash Core</Title>
<Company>PepperDash Technologies</Company>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/PepperDash/PepperDashCore</RepositoryUrl>
<PackageTags>crestron;4series;</PackageTags>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<InformationalVersion>$(Version)</InformationalVersion>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugType>full</DebugType>
<DefineConstants>TRACE;DEBUG;SERIES4</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<DocumentationFile>bin\4Series\$(Configuration)\PepperDashCore.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="lib\**" />
<Compile Remove="Properties\**" />
<EmbeddedResource Remove="lib\**" />
<EmbeddedResource Remove="Properties\**" />
<None Remove="lib\**" />
<None Remove="Properties\**" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" />
<PackageReference Include="Serilog" Version="3.1.1" />
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net6'">
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Comm\._GenericSshClient.cs" />
<Compile Remove="Comm\._GenericTcpIpClient.cs" />
<Compile Remove="Comm\DynamicTCPServer.cs" />
<Compile Remove="PasswordManagement\OLD-ARRAY-Config.cs" />
<Compile Remove="PasswordManagement\OLD-ARRAY-PasswordClient.cs" />
<Compile Remove="PasswordManagement\OLD-ARRAY-PasswordManager.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
<Project>
<PropertyGroup>
<Version>2.0.0-local</Version>
<Authors>PepperDash Technologies</Authors>
<Company>PepperDash Technologies</Company>
<Product>PepperDash Essentials</Product>
<Copyright>Copyright © 2025</Copyright>
<RepositoryType>git</RepositoryType>
<PackageTags>Crestron; 4series</PackageTags>
<PackageOutputPath>../../output</PackageOutputPath>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\LICENSE.md" Pack="true" PackagePath=""/>
<None Include="..\..\README.md" Pack="true" PackagePath=""/>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,57 @@
<Project>
<ItemGroup>
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" Condition="$(ProjectType) == 'Program'">
<Pack>true</Pack>
<PackagePath>build;</PackagePath>
</None>
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" Condition="$(ProjectType) == 'ProgramLibrary'">
<Pack>true</Pack>
<PackagePath>build;</PackagePath>
</None>
<None Include="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).clz" Condition="$(ProjectType) == 'Library'">
<Pack>true</Pack>
<PackagePath>build;</PackagePath>
</None>
</ItemGroup>
<PropertyGroup Condition="$(ProjectType) == 'ProgramLibrary'">
<FileName>$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz</FileName>
</PropertyGroup>
<PropertyGroup Condition="$(ProjectType) == 'Program'">
<FileName>$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz</FileName>
</PropertyGroup>
<Target Name="DeleteCPLZ" BeforeTargets="PreBuildEvent" Condition="$(ProjectType) == 'ProgramLibrary' And $(TargetDir) != '' And Exists($(FileName))">
<Delete Files="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz">
<Output TaskParameter="DeletedFiles" ItemName="DeletedList"/>
</Delete>
<Message Text="Deleted files: '@(DeletedList)'" />
</Target>
<Target Name="CreateCPLZ" AfterTargets="Build" Condition="$(ProjectType) == 'ProgramLibrary' And $(TargetDir) != ''" DependsOnTargets="DeleteCPLZ">
<Message Text="Creating CPLZ $(TargetDir)"></Message>
<MakeDir Directories="$(PackageOutputPath)" Condition="!Exists($(PackageOutputPath))" />
<ZipDirectory SourceDirectory="$(TargetDir)" DestinationFile="$(PackageOutputPath)\$(TargetName).$(Version).$(TargetFramework).cplz" Overwrite="true"/>
<Copy SourceFiles="$(PackageOutputPath)\$(TargetName).$(Version).$(TargetFramework).cplz" DestinationFiles="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz" />
</Target>
<Target Name="DeleteCPZ" BeforeTargets="PreBuildEvent" Condition="$(ProjectType) == 'Program' And $(TargetDir) != '' And Exists($(FileName))">
<Delete Files="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz">
<Output TaskParameter="DeletedFiles" ItemName="DeletedList"/>
</Delete>
<Message Text="Deleted files: '@(DeletedList)'" />
</Target>
<Target Name="Copy CPZ" AfterTargets="SimplSharpPostProcess" Condition="($(ProjectType) == 'Program' And ( '$(TargetFramework)' != 'net6.0' ) And ( '$(TargetFramework)' != 'net8.0' ))">
<Message Text="Copying CPZ"></Message>
<Move SourceFiles="$(TargetDir)$(TargetName).cpz" DestinationFiles="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" />
<Copy SourceFiles="$(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz" DestinationFiles="$(PackageOutputPath)\$(TargetName).$(Version).$(TargetFramework).cpz" />
</Target>
<Target Name="Copy CLZ" AfterTargets="SimplSharpPostProcess" Condition="$(ProjectType) == 'Library'">>
<Message Text="Copying CLZ"></Message>
<Move SourceFiles="$(TargetDir)\$(TargetName).clz" DestinationFiles="$(TargetDir)\$(TargetName).$(Version).clz"/>
<Copy SourceFiles="$(TargetDir)\$(TargetName).$(Version).clz" DestinationFiles="$(PackageOutputPath)\$(TargetName).$(Version).clz"/>
</Target>
<Target Name="SimplSharpNewtonsoft" BeforeTargets="FindReferenceAssembliesForReferences;ResolveReferences">
<ItemGroup>
<ReferencePath Condition="'%(FileName)' == 'Newtonsoft.Json.Compact'">
<Aliases>doNotUse</Aliases>
</ReferencePath>
</ItemGroup>
</Target>
</Project>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<ControlSystem>
<Name>MC3 SSH</Name>
<Address>ssh 10.0.0.15</Address>
<ProgramSlot>Program01</ProgramSlot>
<Storage>Internal Flash</Storage>
</ControlSystem>

View File

@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.SystemInfo
{
/// <summary>
/// Constants
/// </summary>
public class SystemInfoConstants
{
/// <summary>
///
/// </summary>
public const ushort BoolValueChange = 1;
/// <summary>
///
/// </summary>
public const ushort CompleteBoolChange = 2;
/// <summary>
///
/// </summary>
public const ushort BusyBoolChange = 3;
/// <summary>
///
/// </summary>
public const ushort UshortValueChange = 101;
/// <summary>
///
/// </summary>
public const ushort StringValueChange = 201;
/// <summary>
///
/// </summary>
public const ushort ConsoleResponseChange = 202;
/// <summary>
///
/// </summary>
public const ushort ProcessorUptimeChange = 203;
/// <summary>
///
/// </summary>
public const ushort ProgramUptimeChange = 204;
/// <summary>
///
/// </summary>
public const ushort ObjectChange = 301;
/// <summary>
///
/// </summary>
public const ushort ProcessorConfigChange = 302;
/// <summary>
///
/// </summary>
public const ushort EthernetConfigChange = 303;
/// <summary>
///
/// </summary>
public const ushort ControlSubnetConfigChange = 304;
/// <summary>
///
/// </summary>
public const ushort ProgramConfigChange = 305;
}
/// <summary>
/// Processor Change Event Args Class
/// </summary>
public class ProcessorChangeEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ProcessorInfo Processor { get; set; }
/// <summary>
///
/// </summary>
public ushort Type { get; set; }
/// <summary>
///
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ProcessorChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
public ProcessorChangeEventArgs(ProcessorInfo processor, ushort type)
{
Processor = processor;
Type = type;
}
/// <summary>
/// Constructor
/// </summary>
public ProcessorChangeEventArgs(ProcessorInfo processor, ushort type, ushort index)
{
Processor = processor;
Type = type;
Index = index;
}
}
/// <summary>
/// Ethernet Change Event Args Class
/// </summary>
public class EthernetChangeEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public EthernetInfo Adapter { get; set; }
/// <summary>
///
/// </summary>
public ushort Type { get; set; }
/// <summary>
///
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public EthernetChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="ethernet"></param>
/// <param name="type"></param>
public EthernetChangeEventArgs(EthernetInfo ethernet, ushort type)
{
Adapter = ethernet;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="ethernet"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public EthernetChangeEventArgs(EthernetInfo ethernet, ushort type, ushort index)
{
Adapter = ethernet;
Type = type;
Index = index;
}
}
/// <summary>
/// Control Subnet Chage Event Args Class
/// </summary>
public class ControlSubnetChangeEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ControlSubnetInfo Adapter { get; set; }
/// <summary>
///
/// </summary>
public ushort Type { get; set; }
/// <summary>
///
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ControlSubnetChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
public ControlSubnetChangeEventArgs(ControlSubnetInfo controlSubnet, ushort type)
{
Adapter = controlSubnet;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
public ControlSubnetChangeEventArgs(ControlSubnetInfo controlSubnet, ushort type, ushort index)
{
Adapter = controlSubnet;
Type = type;
Index = index;
}
}
/// <summary>
/// Program Change Event Args Class
/// </summary>
public class ProgramChangeEventArgs : EventArgs
{
/// <summary>
///
/// </summary>
public ProgramInfo Program { get; set; }
/// <summary>
///
/// </summary>
public ushort Type { get; set; }
/// <summary>
///
/// </summary>
public ushort Index { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ProgramChangeEventArgs()
{
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="program"></param>
/// <param name="type"></param>
public ProgramChangeEventArgs(ProgramInfo program, ushort type)
{
Program = program;
Type = type;
}
/// <summary>
/// Constructor overload
/// </summary>
/// <param name="program"></param>
/// <param name="type"></param>
/// <param name="index"></param>
public ProgramChangeEventArgs(ProgramInfo program, ushort type, ushort index)
{
Program = program;
Type = type;
Index = index;
}
}
}

View File

@@ -0,0 +1,204 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.SystemInfo
{
/// <summary>
/// Processor info class
/// </summary>
public class ProcessorInfo
{
/// <summary>
///
/// </summary>
public string Model { get; set; }
/// <summary>
///
/// </summary>
public string SerialNumber { get; set; }
/// <summary>
///
/// </summary>
public string Firmware { get; set; }
/// <summary>
///
/// </summary>
public string FirmwareDate { get; set; }
/// <summary>
///
/// </summary>
public string OsVersion { get; set; }
/// <summary>
///
/// </summary>
public string RuntimeEnvironment { get; set; }
/// <summary>
///
/// </summary>
public string DevicePlatform { get; set; }
/// <summary>
///
/// </summary>
public string ModuleDirectory { get; set; }
/// <summary>
///
/// </summary>
public string LocalTimeZone { get; set; }
/// <summary>
///
/// </summary>
public string ProgramIdTag { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ProcessorInfo()
{
}
}
/// <summary>
/// Ethernet info class
/// </summary>
public class EthernetInfo
{
/// <summary>
///
/// </summary>
public ushort DhcpIsOn { get; set; }
/// <summary>
///
/// </summary>
public string Hostname { get; set; }
/// <summary>
///
/// </summary>
public string MacAddress { get; set; }
/// <summary>
///
/// </summary>
public string IpAddress { get; set; }
/// <summary>
///
/// </summary>
public string Subnet { get; set; }
/// <summary>
///
/// </summary>
public string Gateway { get; set; }
/// <summary>
///
/// </summary>
public string Dns1 { get; set; }
/// <summary>
///
/// </summary>
public string Dns2 { get; set; }
/// <summary>
///
/// </summary>
public string Dns3 { get; set; }
/// <summary>
///
/// </summary>
public string Domain { get; set; }
/// <summary>
/// Constructor
/// </summary>
public EthernetInfo()
{
}
}
/// <summary>
/// Control subnet info class
/// </summary>
public class ControlSubnetInfo
{
/// <summary>
///
/// </summary>
public ushort Enabled { get; set; }
/// <summary>
///
/// </summary>
public ushort IsInAutomaticMode { get; set; }
/// <summary>
///
/// </summary>
public string MacAddress { get; set; }
/// <summary>
///
/// </summary>
public string IpAddress { get; set; }
/// <summary>
///
/// </summary>
public string Subnet { get; set; }
/// <summary>
///
/// </summary>
public string RouterPrefix { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ControlSubnetInfo()
{
}
}
/// <summary>
/// Program info class
/// </summary>
public class ProgramInfo
{
/// <summary>
///
/// </summary>
public string Name { get; set; }
/// <summary>
///
/// </summary>
public string Header { get; set; }
/// <summary>
///
/// </summary>
public string System { get; set; }
/// <summary>
///
/// </summary>
public string ProgramIdTag { get; set; }
/// <summary>
///
/// </summary>
public string CompileTime { get; set; }
/// <summary>
///
/// </summary>
public string Database { get; set; }
/// <summary>
///
/// </summary>
public string Environment { get; set; }
/// <summary>
///
/// </summary>
public string Programmer { get; set; }
/// <summary>
/// Constructor
/// </summary>
public ProgramInfo()
{
}
}
}

View File

@@ -0,0 +1,462 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.SystemInfo
{
/// <summary>
/// System Info class
/// </summary>
public class SystemInfoToSimpl
{
/// <summary>
/// Notifies of bool change
/// </summary>
public event EventHandler<BoolChangeEventArgs> BoolChange;
/// <summary>
/// Notifies of string change
/// </summary>
public event EventHandler<StringChangeEventArgs> StringChange;
/// <summary>
/// Notifies of processor change
/// </summary>
public event EventHandler<ProcessorChangeEventArgs> ProcessorChange;
/// <summary>
/// Notifies of ethernet change
/// </summary>
public event EventHandler<EthernetChangeEventArgs> EthernetChange;
/// <summary>
/// Notifies of control subnet change
/// </summary>
public event EventHandler<ControlSubnetChangeEventArgs> ControlSubnetChange;
/// <summary>
/// Notifies of program change
/// </summary>
public event EventHandler<ProgramChangeEventArgs> ProgramChange;
/// <summary>
/// Constructor
/// </summary>
public SystemInfoToSimpl()
{
}
/// <summary>
/// Gets the current processor info
/// </summary>
public void GetProcessorInfo()
{
OnBoolChange(true, 0, SystemInfoConstants.BusyBoolChange);
try
{
var processor = new ProcessorInfo();
processor.Model = InitialParametersClass.ControllerPromptName;
processor.SerialNumber = CrestronEnvironment.SystemInfo.SerialNumber;
processor.ModuleDirectory = InitialParametersClass.ProgramDirectory.ToString();
processor.ProgramIdTag = InitialParametersClass.ProgramIDTag;
processor.DevicePlatform = CrestronEnvironment.DevicePlatform.ToString();
processor.OsVersion = CrestronEnvironment.OSVersion.Version.ToString();
processor.RuntimeEnvironment = CrestronEnvironment.RuntimeEnvironment.ToString();
processor.LocalTimeZone = CrestronEnvironment.GetTimeZone().Offset;
// Does not return firmware version matching a "ver" command
// returns the "ver -v" 'CAB' version
// example return ver -v:
// RMC3 Cntrl Eng [v1.503.3568.25373 (Oct 09 2018), #4001E302] @E-00107f4420f0
// Build: 14:05:46 Oct 09 2018 (3568.25373)
// Cab: 1.503.0070
// Applications: 1.0.6855.21351
// Updater: 1.4.24
// Bootloader: 1.22.00
// RMC3-SetupProgram: 1.003.0011
// IOPVersion: FPGA [v09] slot:7
// PUF: Unknown
//Firmware = CrestronEnvironment.OSVersion.Firmware;
//Firmware = InitialParametersClass.FirmwareVersion;
// Use below logic to get actual firmware ver, not the 'CAB' returned by the above
// matches console return of a "ver" and on SystemInfo page
// example return ver:
// RMC3 Cntrl Eng [v1.503.3568.25373 (Oct 09 2018), #4001E302] @E-00107f4420f0
var response = "";
CrestronConsole.SendControlSystemCommand("ver", ref response);
processor.Firmware = ParseConsoleResponse(response, "Cntrl Eng", "[", "(");
processor.FirmwareDate = ParseConsoleResponse(response, "Cntrl Eng", "(", ")");
OnProcessorChange(processor, 0, SystemInfoConstants.ProcessorConfigChange);
}
catch (Exception e)
{
var msg = string.Format("GetProcessorInfo failed: {0}", e.Message);
CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
}
OnBoolChange(false, 0, SystemInfoConstants.BusyBoolChange);
}
/// <summary>
/// Gets the current ethernet info
/// </summary>
public void GetEthernetInfo()
{
OnBoolChange(true, 0, SystemInfoConstants.BusyBoolChange);
var adapter = new EthernetInfo();
try
{
// get lan adapter id
var adapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter);
// get lan adapter info
var dhcpState = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, adapterId);
if (!string.IsNullOrEmpty(dhcpState))
adapter.DhcpIsOn = (ushort)(dhcpState.ToLower().Contains("on") ? 1 : 0);
adapter.Hostname = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, adapterId);
adapter.MacAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, adapterId);
adapter.IpAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterId);
adapter.Subnet = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterId);
adapter.Gateway = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, adapterId);
adapter.Domain = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DOMAIN_NAME, adapterId);
// returns comma seperate list of dns servers with trailing comma
// example return: "8.8.8.8 (DHCP),8.8.4.4 (DHCP),"
string dns = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_DNS_SERVER, adapterId);
if (dns.Contains(","))
{
string[] dnsList = dns.Split(',');
for (var i = 0; i < dnsList.Length; i++)
{
if(i == 0)
adapter.Dns1 = !string.IsNullOrEmpty(dnsList[0]) ? dnsList[0] : "0.0.0.0";
if(i == 1)
adapter.Dns2 = !string.IsNullOrEmpty(dnsList[1]) ? dnsList[1] : "0.0.0.0";
if(i == 2)
adapter.Dns3 = !string.IsNullOrEmpty(dnsList[2]) ? dnsList[2] : "0.0.0.0";
}
}
else
{
adapter.Dns1 = !string.IsNullOrEmpty(dns) ? dns : "0.0.0.0";
adapter.Dns2 = "0.0.0.0";
adapter.Dns3 = "0.0.0.0";
}
OnEthernetInfoChange(adapter, 0, SystemInfoConstants.EthernetConfigChange);
}
catch (Exception e)
{
var msg = string.Format("GetEthernetInfo failed: {0}", e.Message);
CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
}
OnBoolChange(false, 0, SystemInfoConstants.BusyBoolChange);
}
/// <summary>
/// Gets the current control subnet info
/// </summary>
public void GetControlSubnetInfo()
{
OnBoolChange(true, 0, SystemInfoConstants.BusyBoolChange);
var adapter = new ControlSubnetInfo();
try
{
// get cs adapter id
var adapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter);
if (!adapterId.Equals(EthernetAdapterType.EthernetUnknownAdapter))
{
adapter.Enabled = 1;
adapter.IsInAutomaticMode = (ushort)(CrestronEthernetHelper.IsControlSubnetInAutomaticMode ? 1 : 0);
adapter.MacAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_MAC_ADDRESS, adapterId);
adapter.IpAddress = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, adapterId);
adapter.Subnet = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, adapterId);
adapter.RouterPrefix = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CONTROL_SUBNET_ROUTER_PREFIX, adapterId);
}
}
catch (Exception e)
{
adapter.Enabled = 0;
adapter.IsInAutomaticMode = 0;
adapter.MacAddress = "NA";
adapter.IpAddress = "NA";
adapter.Subnet = "NA";
adapter.RouterPrefix = "NA";
var msg = string.Format("GetControlSubnetInfo failed: {0}", e.Message);
CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
}
OnControlSubnetInfoChange(adapter, 0, SystemInfoConstants.ControlSubnetConfigChange);
OnBoolChange(false, 0, SystemInfoConstants.BusyBoolChange);
}
/// <summary>
/// Gets the program info by index
/// </summary>
/// <param name="index"></param>
public void GetProgramInfoByIndex(ushort index)
{
if (index < 1 || index > 10)
return;
OnBoolChange(true, 0, SystemInfoConstants.BusyBoolChange);
var program = new ProgramInfo();
try
{
var response = "";
CrestronConsole.SendControlSystemCommand(string.Format("progcomments:{0}", index), ref response);
// no program loaded or running
if (response.Contains("Bad or Incomplete Command"))
{
program.Name = "";
program.System = "";
program.Programmer = "";
program.CompileTime = "";
program.Database = "";
program.Environment = "";
}
else
{
// SIMPL returns
program.Name = ParseConsoleResponse(response, "Program File", ":", "\x0D");
program.System = ParseConsoleResponse(response, "System Name", ":", "\x0D");
program.ProgramIdTag = ParseConsoleResponse(response, "Friendly Name", ":", "\x0D");
program.Programmer = ParseConsoleResponse(response, "Programmer", ":", "\x0D");
program.CompileTime = ParseConsoleResponse(response, "Compiled On", ":", "\x0D");
program.Database = ParseConsoleResponse(response, "CrestronDB", ":", "\x0D");
program.Environment = ParseConsoleResponse(response, "Source Env", ":", "\x0D");
// S# returns
if (program.System.Length == 0)
program.System = ParseConsoleResponse(response, "Application Name", ":", "\x0D");
if (program.Database.Length == 0)
program.Database = ParseConsoleResponse(response, "PlugInVersion", ":", "\x0D");
if (program.Environment.Length == 0)
program.Environment = ParseConsoleResponse(response, "Program Tool", ":", "\x0D");
}
OnProgramChange(program, index, SystemInfoConstants.ProgramConfigChange);
}
catch (Exception e)
{
var msg = string.Format("GetProgramInfoByIndex failed: {0}", e.Message);
CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
}
OnBoolChange(false, 0, SystemInfoConstants.BusyBoolChange);
}
/// <summary>
/// Gets the processor uptime and passes it to S+
/// </summary>
public void RefreshProcessorUptime()
{
try
{
string response = "";
CrestronConsole.SendControlSystemCommand("uptime", ref response);
var uptime = ParseConsoleResponse(response, "running for", "running for", "\x0D");
OnStringChange(uptime, 0, SystemInfoConstants.ProcessorUptimeChange);
}
catch (Exception e)
{
var msg = string.Format("RefreshProcessorUptime failed:\r{0}", e.Message);
CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
}
}
/// <summary>
/// Gets the program uptime, by index, and passes it to S+
/// </summary>
/// <param name="index"></param>
public void RefreshProgramUptimeByIndex(int index)
{
try
{
string response = "";
CrestronConsole.SendControlSystemCommand(string.Format("proguptime:{0}", index), ref response);
string uptime = ParseConsoleResponse(response, "running for", "running for", "\x0D");
OnStringChange(uptime, (ushort)index, SystemInfoConstants.ProgramUptimeChange);
}
catch (Exception e)
{
var msg = string.Format("RefreshProgramUptimebyIndex({0}) failed:\r{1}", index, e.Message);
CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
}
}
/// <summary>
/// Sends command to console, passes response back using string change event
/// </summary>
/// <param name="cmd"></param>
public void SendConsoleCommand(string cmd)
{
if (string.IsNullOrEmpty(cmd))
return;
string response = "";
CrestronConsole.SendControlSystemCommand(cmd, ref response);
if (!string.IsNullOrEmpty(response))
{
if (response.EndsWith("\x0D\\x0A"))
response.Trim('\n');
OnStringChange(response, 0, SystemInfoConstants.ConsoleResponseChange);
}
}
/// <summary>
/// private method to parse console messages
/// </summary>
/// <param name="data"></param>
/// <param name="line"></param>
/// <param name="dataStart"></param>
/// <param name="dataEnd"></param>
/// <returns></returns>
private string ParseConsoleResponse(string data, string line, string dataStart, string dataEnd)
{
var response = "";
if (string.IsNullOrEmpty(data) || string.IsNullOrEmpty(line) || string.IsNullOrEmpty(dataStart) || string.IsNullOrEmpty(dataEnd))
return response;
try
{
var linePos = data.IndexOf(line);
var startPos = data.IndexOf(dataStart, linePos) + dataStart.Length;
var endPos = data.IndexOf(dataEnd, startPos);
response = data.Substring(startPos, endPos - startPos).Trim();
}
catch (Exception e)
{
var msg = string.Format("ParseConsoleResponse failed: {0}", e.Message);
CrestronConsole.PrintLine(msg);
//ErrorLog.Error(msg);
}
return response;
}
/// <summary>
/// Protected boolean change event handler
/// </summary>
/// <param name="state"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnBoolChange(bool state, ushort index, ushort type)
{
var handler = BoolChange;
if (handler != null)
{
var args = new BoolChangeEventArgs(state, type);
args.Index = index;
BoolChange(this, args);
}
}
/// <summary>
/// Protected string change event handler
/// </summary>
/// <param name="value"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnStringChange(string value, ushort index, ushort type)
{
var handler = StringChange;
if (handler != null)
{
var args = new StringChangeEventArgs(value, type);
args.Index = index;
StringChange(this, args);
}
}
/// <summary>
/// Protected processor config change event handler
/// </summary>
/// <param name="processor"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnProcessorChange(ProcessorInfo processor, ushort index, ushort type)
{
var handler = ProcessorChange;
if (handler != null)
{
var args = new ProcessorChangeEventArgs(processor, type);
args.Index = index;
ProcessorChange(this, args);
}
}
/// <summary>
/// Ethernet change event handler
/// </summary>
/// <param name="ethernet"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnEthernetInfoChange(EthernetInfo ethernet, ushort index, ushort type)
{
var handler = EthernetChange;
if (handler != null)
{
var args = new EthernetChangeEventArgs(ethernet, type);
args.Index = index;
EthernetChange(this, args);
}
}
/// <summary>
/// Control Subnet change event handler
/// </summary>
/// <param name="ethernet"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnControlSubnetInfoChange(ControlSubnetInfo ethernet, ushort index, ushort type)
{
var handler = ControlSubnetChange;
if (handler != null)
{
var args = new ControlSubnetChangeEventArgs(ethernet, type);
args.Index = index;
ControlSubnetChange(this, args);
}
}
/// <summary>
/// Program change event handler
/// </summary>
/// <param name="program"></param>
/// <param name="index"></param>
/// <param name="type"></param>
protected void OnProgramChange(ProgramInfo program, ushort index, ushort type)
{
var handler = ProgramChange;
if (handler != null)
{
var args = new ProgramChangeEventArgs(program, type);
args.Index = index;
ProgramChange(this, args);
}
}
}
}

View File

@@ -0,0 +1,356 @@
using Crestron.SimplSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Prng;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.Utilities;
using Org.BouncyCastle.X509;
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
using X509KeyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags;
using X509ContentType = System.Security.Cryptography.X509Certificates.X509ContentType;
using Org.BouncyCastle.Crypto.Operators;
using BigInteger = Org.BouncyCastle.Math.BigInteger;
using X509Certificate = Org.BouncyCastle.X509.X509Certificate;
namespace PepperDash.Core
{
/// <summary>
/// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/
/// </summary>
internal class BouncyCertificate
{
public string CertificatePassword { get; set; } = "password";
public X509Certificate2 LoadCertificate(string issuerFileName, string password)
{
// We need to pass 'Exportable', otherwise we can't get the private key.
var issuerCertificate = new X509Certificate2(issuerFileName, password, X509KeyStorageFlags.Exportable);
return issuerCertificate;
}
public X509Certificate2 IssueCertificate(string subjectName, X509Certificate2 issuerCertificate, string[] subjectAlternativeNames, KeyPurposeID[] usages)
{
// It's self-signed, so these are the same.
var issuerName = issuerCertificate.Subject;
var random = GetSecureRandom();
var subjectKeyPair = GenerateKeyPair(random, 2048);
var issuerKeyPair = DotNetUtilities.GetKeyPair(issuerCertificate.PrivateKey);
var serialNumber = GenerateSerialNumber(random);
var issuerSerialNumber = new BigInteger(issuerCertificate.GetSerialNumber());
const bool isCertificateAuthority = false;
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
subjectAlternativeNames, issuerName, issuerKeyPair,
issuerSerialNumber, isCertificateAuthority,
usages);
return ConvertCertificate(certificate, subjectKeyPair, random);
}
public X509Certificate2 CreateCertificateAuthorityCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
{
// It's self-signed, so these are the same.
var issuerName = subjectName;
var random = GetSecureRandom();
var subjectKeyPair = GenerateKeyPair(random, 2048);
// It's self-signed, so these are the same.
var issuerKeyPair = subjectKeyPair;
var serialNumber = GenerateSerialNumber(random);
var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
const bool isCertificateAuthority = true;
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
subjectAlternativeNames, issuerName, issuerKeyPair,
issuerSerialNumber, isCertificateAuthority,
usages);
return ConvertCertificate(certificate, subjectKeyPair, random);
}
public X509Certificate2 CreateSelfSignedCertificate(string subjectName, string[] subjectAlternativeNames, KeyPurposeID[] usages)
{
// It's self-signed, so these are the same.
var issuerName = subjectName;
var random = GetSecureRandom();
var subjectKeyPair = GenerateKeyPair(random, 2048);
// It's self-signed, so these are the same.
var issuerKeyPair = subjectKeyPair;
var serialNumber = GenerateSerialNumber(random);
var issuerSerialNumber = serialNumber; // Self-signed, so it's the same serial number.
const bool isCertificateAuthority = false;
var certificate = GenerateCertificate(random, subjectName, subjectKeyPair, serialNumber,
subjectAlternativeNames, issuerName, issuerKeyPair,
issuerSerialNumber, isCertificateAuthority,
usages);
return ConvertCertificate(certificate, subjectKeyPair, random);
}
private SecureRandom GetSecureRandom()
{
// Since we're on Windows, we'll use the CryptoAPI one (on the assumption
// that it might have access to better sources of entropy than the built-in
// Bouncy Castle ones):
var randomGenerator = new CryptoApiRandomGenerator();
var random = new SecureRandom(randomGenerator);
return random;
}
private X509Certificate GenerateCertificate(SecureRandom random,
string subjectName,
AsymmetricCipherKeyPair subjectKeyPair,
BigInteger subjectSerialNumber,
string[] subjectAlternativeNames,
string issuerName,
AsymmetricCipherKeyPair issuerKeyPair,
BigInteger issuerSerialNumber,
bool isCertificateAuthority,
KeyPurposeID[] usages)
{
var certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.SetSerialNumber(subjectSerialNumber);
var issuerDN = new X509Name(issuerName);
certificateGenerator.SetIssuerDN(issuerDN);
// Note: The subject can be omitted if you specify a subject alternative name (SAN).
var subjectDN = new X509Name(subjectName);
certificateGenerator.SetSubjectDN(subjectDN);
// Our certificate needs valid from/to values.
var notBefore = DateTime.UtcNow.Date;
var notAfter = notBefore.AddYears(2);
certificateGenerator.SetNotBefore(notBefore);
certificateGenerator.SetNotAfter(notAfter);
// The subject's public key goes in the certificate.
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
AddAuthorityKeyIdentifier(certificateGenerator, issuerDN, issuerKeyPair, issuerSerialNumber);
AddSubjectKeyIdentifier(certificateGenerator, subjectKeyPair);
//AddBasicConstraints(certificateGenerator, isCertificateAuthority);
if (usages != null && usages.Any())
AddExtendedKeyUsage(certificateGenerator, usages);
if (subjectAlternativeNames != null && subjectAlternativeNames.Any())
AddSubjectAlternativeNames(certificateGenerator, subjectAlternativeNames);
// Set the signature algorithm. This is used to generate the thumbprint which is then signed
// with the issuer's private key. We'll use SHA-256, which is (currently) considered fairly strong.
const string signatureAlgorithm = "SHA256WithRSA";
// The certificate is signed with the issuer's private key.
ISignatureFactory signatureFactory = new Asn1SignatureFactory(signatureAlgorithm, issuerKeyPair.Private, random);
var certificate = certificateGenerator.Generate(signatureFactory);
return certificate;
}
/// <summary>
/// The certificate needs a serial number. This is used for revocation,
/// and usually should be an incrementing index (which makes it easier to revoke a range of certificates).
/// Since we don't have anywhere to store the incrementing index, we can just use a random number.
/// </summary>
/// <param name="random"></param>
/// <returns></returns>
private BigInteger GenerateSerialNumber(SecureRandom random)
{
var serialNumber =
BigIntegers.CreateRandomInRange(
BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
return serialNumber;
}
/// <summary>
/// Generate a key pair.
/// </summary>
/// <param name="random">The random number generator.</param>
/// <param name="strength">The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days.</param>
/// <returns></returns>
private AsymmetricCipherKeyPair GenerateKeyPair(SecureRandom random, int strength)
{
var keyGenerationParameters = new KeyGenerationParameters(random, strength);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);
var subjectKeyPair = keyPairGenerator.GenerateKeyPair();
return subjectKeyPair;
}
/// <summary>
/// Add the Authority Key Identifier. According to http://www.alvestrand.no/objectid/2.5.29.35.html, this
/// identifies the public key to be used to verify the signature on this certificate.
/// In a certificate chain, this corresponds to the "Subject Key Identifier" on the *issuer* certificate.
/// The Bouncy Castle documentation, at http://www.bouncycastle.org/wiki/display/JA1/X.509+Public+Key+Certificate+and+Certification+Request+Generation,
/// shows how to create this from the issuing certificate. Since we're creating a self-signed certificate, we have to do this slightly differently.
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="issuerDN"></param>
/// <param name="issuerKeyPair"></param>
/// <param name="issuerSerialNumber"></param>
private void AddAuthorityKeyIdentifier(X509V3CertificateGenerator certificateGenerator,
X509Name issuerDN,
AsymmetricCipherKeyPair issuerKeyPair,
BigInteger issuerSerialNumber)
{
var authorityKeyIdentifierExtension =
new AuthorityKeyIdentifier(
SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(issuerKeyPair.Public),
new GeneralNames(new GeneralName(issuerDN)),
issuerSerialNumber);
certificateGenerator.AddExtension(
X509Extensions.AuthorityKeyIdentifier.Id, false, authorityKeyIdentifierExtension);
}
/// <summary>
/// Add the "Subject Alternative Names" extension. Note that you have to repeat
/// the value from the "Subject Name" property.
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="subjectAlternativeNames"></param>
private void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator,
IEnumerable<string> subjectAlternativeNames)
{
var subjectAlternativeNamesExtension =
new DerSequence(
subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name))
.ToArray<Asn1Encodable>());
certificateGenerator.AddExtension(
X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension);
}
/// <summary>
/// Add the "Extended Key Usage" extension, specifying (for example) "server authentication".
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="usages"></param>
private void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages)
{
certificateGenerator.AddExtension(
X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages));
}
/// <summary>
/// Add the "Basic Constraints" extension.
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="isCertificateAuthority"></param>
private void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator,
bool isCertificateAuthority)
{
certificateGenerator.AddExtension(
X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority));
}
/// <summary>
/// Add the Subject Key Identifier.
/// </summary>
/// <param name="certificateGenerator"></param>
/// <param name="subjectKeyPair"></param>
private void AddSubjectKeyIdentifier(X509V3CertificateGenerator certificateGenerator,
AsymmetricCipherKeyPair subjectKeyPair)
{
var subjectKeyIdentifierExtension =
new SubjectKeyIdentifier(
SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(subjectKeyPair.Public));
certificateGenerator.AddExtension(
X509Extensions.SubjectKeyIdentifier.Id, false, subjectKeyIdentifierExtension);
}
private X509Certificate2 ConvertCertificate(X509Certificate certificate,
AsymmetricCipherKeyPair subjectKeyPair,
SecureRandom random)
{
// Now to convert the Bouncy Castle certificate to a .NET certificate.
// See http://web.archive.org/web/20100504192226/http://www.fkollmann.de/v2/post/Creating-certificates-using-BouncyCastle.aspx
// ...but, basically, we create a PKCS12 store (a .PFX file) in memory, and add the public and private key to that.
var store = new Pkcs12StoreBuilder().Build();
// What Bouncy Castle calls "alias" is the same as what Windows terms the "friendly name".
string friendlyName = certificate.SubjectDN.ToString();
// Add the certificate.
var certificateEntry = new X509CertificateEntry(certificate);
store.SetCertificateEntry(friendlyName, certificateEntry);
// Add the private key.
store.SetKeyEntry(friendlyName, new AsymmetricKeyEntry(subjectKeyPair.Private), new[] { certificateEntry });
// Convert it to an X509Certificate2 object by saving/loading it from a MemoryStream.
// It needs a password. Since we'll remove this later, it doesn't particularly matter what we use.
var stream = new MemoryStream();
store.Save(stream, CertificatePassword.ToCharArray(), random);
var convertedCertificate =
new X509Certificate2(stream.ToArray(),
CertificatePassword,
X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable);
return convertedCertificate;
}
public void WriteCertificate(X509Certificate2 certificate, string outputDirectory, string certName)
{
// This password is the one attached to the PFX file. Use 'null' for no password.
// Create PFX (PKCS #12) with private key
try
{
var pfx = certificate.Export(X509ContentType.Pfx, CertificatePassword);
File.WriteAllBytes(string.Format("{0}.pfx", Path.Combine(outputDirectory, certName)), pfx);
}
catch (Exception ex)
{
CrestronConsole.PrintLine(string.Format("Failed to write x509 cert pfx\r\n{0}", ex.Message));
}
// Create Base 64 encoded CER (public key only)
using (var writer = new StreamWriter($"{Path.Combine(outputDirectory, certName)}.cer", false))
{
try
{
var contents = string.Format("-----BEGIN CERTIFICATE-----\r\n{0}\r\n-----END CERTIFICATE-----", Convert.ToBase64String(certificate.Export(X509ContentType.Cert), Base64FormattingOptions.InsertLineBreaks));
writer.Write(contents);
}
catch (Exception ex)
{
CrestronConsole.PrintLine(string.Format("Failed to write x509 cert cer\r\n{0}", ex.Message));
}
}
}
public bool AddCertToStore(X509Certificate2 cert, System.Security.Cryptography.X509Certificates.StoreName st, System.Security.Cryptography.X509Certificates.StoreLocation sl)
{
bool bRet = false;
try
{
var store = new System.Security.Cryptography.X509Certificates.X509Store(st, sl);
store.Open(System.Security.Cryptography.X509Certificates.OpenFlags.ReadWrite);
store.Add(cert);
store.Close();
bRet = true;
}
catch (Exception ex)
{
CrestronConsole.PrintLine(string.Format("AddCertToStore Failed\r\n{0}\r\n{1}", ex.Message, ex.StackTrace));
}
return bRet;
}
}
}

View File

@@ -0,0 +1,17 @@
using Crestron.SimplSharp.WebScripting;
namespace PepperDash.Core.Web.RequestHandlers
{
/// <summary>
/// Web API default request handler
/// </summary>
public class DefaultRequestHandler : WebApiBaseRequestHandler
{
/// <summary>
/// Constructor
/// </summary>
public DefaultRequestHandler()
: base(true)
{ }
}
}

View File

@@ -0,0 +1,163 @@
using Crestron.SimplSharp.WebScripting;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace PepperDash.Core.Web.RequestHandlers
{
public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
{
private readonly Dictionary<string, Func<HttpCwsContext, Task>> _handlers;
protected readonly bool EnableCors;
/// <summary>
/// Constructor
/// </summary>
protected WebApiBaseRequestAsyncHandler(bool enableCors)
{
EnableCors = enableCors;
_handlers = new Dictionary<string, Func<HttpCwsContext, Task>>
{
{"CONNECT", HandleConnect},
{"DELETE", HandleDelete},
{"GET", HandleGet},
{"HEAD", HandleHead},
{"OPTIONS", HandleOptions},
{"PATCH", HandlePatch},
{"POST", HandlePost},
{"PUT", HandlePut},
{"TRACE", HandleTrace}
};
}
/// <summary>
/// Constructor
/// </summary>
protected WebApiBaseRequestAsyncHandler()
: this(false)
{
}
/// <summary>
/// Handles CONNECT method requests
/// </summary>
/// <param name="context"></param>
protected virtual async Task HandleConnect(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles DELETE method requests
/// </summary>
/// <param name="context"></param>
protected virtual async Task HandleDelete(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles GET method requests
/// </summary>
/// <param name="context"></param>
protected virtual async Task HandleGet(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles HEAD method requests
/// </summary>
/// <param name="context"></param>
protected virtual async Task HandleHead(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles OPTIONS method requests
/// </summary>
/// <param name="context"></param>
protected virtual async Task HandleOptions(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles PATCH method requests
/// </summary>
/// <param name="context"></param>
protected virtual async Task HandlePatch(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles POST method requests
/// </summary>
/// <param name="context"></param>
protected virtual async Task HandlePost(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles PUT method requests
/// </summary>
/// <param name="context"></param>
protected virtual async Task HandlePut(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles TRACE method requests
/// </summary>
/// <param name="context"></param>
protected virtual async Task HandleTrace(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Process request
/// </summary>
/// <param name="context"></param>
public void ProcessRequest(HttpCwsContext context)
{
if (!_handlers.TryGetValue(context.Request.HttpMethod, out Func<HttpCwsContext, Task> handler))
{
return;
}
if (EnableCors)
{
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
context.Response.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
}
var handlerTask = handler(context);
handlerTask.GetAwaiter().GetResult();
}
}
}

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp.WebScripting;
namespace PepperDash.Core.Web.RequestHandlers
{
/// <summary>
/// CWS Base Handler, implements IHttpCwsHandler
/// </summary>
public abstract class WebApiBaseRequestHandler : IHttpCwsHandler
{
private readonly Dictionary<string, Action<HttpCwsContext>> _handlers;
protected readonly bool EnableCors;
/// <summary>
/// Constructor
/// </summary>
protected WebApiBaseRequestHandler(bool enableCors)
{
EnableCors = enableCors;
_handlers = new Dictionary<string, Action<HttpCwsContext>>
{
{"CONNECT", HandleConnect},
{"DELETE", HandleDelete},
{"GET", HandleGet},
{"HEAD", HandleHead},
{"OPTIONS", HandleOptions},
{"PATCH", HandlePatch},
{"POST", HandlePost},
{"PUT", HandlePut},
{"TRACE", HandleTrace}
};
}
/// <summary>
/// Constructor
/// </summary>
protected WebApiBaseRequestHandler()
: this(false)
{
}
/// <summary>
/// Handles CONNECT method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleConnect(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles DELETE method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleDelete(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles GET method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleGet(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles HEAD method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleHead(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles OPTIONS method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleOptions(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles PATCH method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandlePatch(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles POST method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandlePost(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles PUT method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandlePut(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Handles TRACE method requests
/// </summary>
/// <param name="context"></param>
protected virtual void HandleTrace(HttpCwsContext context)
{
context.Response.StatusCode = 501;
context.Response.StatusDescription = "Not Implemented";
context.Response.End();
}
/// <summary>
/// Process request
/// </summary>
/// <param name="context"></param>
public void ProcessRequest(HttpCwsContext context)
{
Action<HttpCwsContext> handler;
if (!_handlers.TryGetValue(context.Request.HttpMethod, out handler))
{
return;
}
if (EnableCors)
{
context.Response.Headers.Add("Access-Control-Allow-Origin", "*");
context.Response.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
}
handler(context);
}
}
}

View File

@@ -0,0 +1,284 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using Crestron.SimplSharp.WebScripting;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core.Web.RequestHandlers;
namespace PepperDash.Core.Web
{
/// <summary>
/// Web API server
/// </summary>
public class WebApiServer : IKeyName
{
private const string SplusKey = "Uninitialized Web API Server";
private const string DefaultName = "Web API Server";
private const string DefaultBasePath = "/api";
private const uint DebugTrace = 0;
private const uint DebugInfo = 1;
private const uint DebugVerbose = 2;
private readonly CCriticalSection _serverLock = new CCriticalSection();
private HttpCwsServer _server;
/// <summary>
/// Web API server key
/// </summary>
public string Key { get; private set; }
/// <summary>
/// Web API server name
/// </summary>
public string Name { get; private set; }
/// <summary>
/// CWS base path, will default to "/api" if not set via initialize method
/// </summary>
public string BasePath { get; private set; }
/// <summary>
/// Indicates CWS is registered with base path
/// </summary>
public bool IsRegistered { get; private set; }
/// <summary>
/// Http request handler
/// </summary>
//public IHttpCwsHandler HttpRequestHandler
//{
// get { return _server.HttpRequestHandler; }
// set
// {
// if (_server == null) return;
// _server.HttpRequestHandler = value;
// }
//}
/// <summary>
/// Received request event handler
/// </summary>
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
//{
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
// remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); }
//}
/// <summary>
/// Constructor for S+. Make sure to set necessary properties using init method
/// </summary>
public WebApiServer()
: this(SplusKey, DefaultName, null)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="basePath"></param>
public WebApiServer(string key, string basePath)
: this(key, DefaultName, basePath)
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="key"></param>
/// <param name="name"></param>
/// <param name="basePath"></param>
public WebApiServer(string key, string name, string basePath)
{
Key = key;
Name = string.IsNullOrEmpty(name) ? DefaultName : name;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
if (_server == null) _server = new HttpCwsServer(BasePath);
_server.setProcessName(Key);
_server.HttpRequestHandler = new DefaultRequestHandler();
CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler;
CrestronEnvironment.EthernetEventHandler += CrestronEnvironment_EthernetEventHandler;
}
/// <summary>
/// Program status event handler
/// </summary>
/// <param name="programEventType"></param>
void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
{
if (programEventType != eProgramStatusEventType.Stopping) return;
Debug.Console(DebugInfo, this, "Program stopping. stopping server");
Stop();
}
/// <summary>
/// Ethernet event handler
/// </summary>
/// <param name="ethernetEventArgs"></param>
void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs ethernetEventArgs)
{
// Re-enable the server if the link comes back up and the status should be connected
if (ethernetEventArgs.EthernetEventType == eEthernetEventType.LinkUp && IsRegistered)
{
Debug.Console(DebugInfo, this, "Ethernet link up. Server is alreedy registered.");
return;
}
Debug.Console(DebugInfo, this, "Ethernet link up. Starting server");
Start();
}
/// <summary>
/// Initializes CWS class
/// </summary>
public void Initialize(string key, string basePath)
{
Key = key;
BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
}
/// <summary>
/// Adds a route to CWS
/// </summary>
public void AddRoute(HttpCwsRoute route)
{
if (route == null)
{
Debug.Console(DebugInfo, this, "Failed to add route, route parameter is null");
return;
}
_server.Routes.Add(route);
}
/// <summary>
/// Removes a route from CWS
/// </summary>
/// <param name="route"></param>
public void RemoveRoute(HttpCwsRoute route)
{
if (route == null)
{
Debug.Console(DebugInfo, this, "Failed to remote route, orute parameter is null");
return;
}
_server.Routes.Remove(route);
}
/// <summary>
/// Returns a list of the current routes
/// </summary>
public HttpCwsRouteCollection GetRouteCollection()
{
return _server.Routes;
}
/// <summary>
/// Starts CWS instance
/// </summary>
public void Start()
{
try
{
_serverLock.Enter();
if (_server == null)
{
Debug.Console(DebugInfo, this, "Server is null, unable to start");
return;
}
if (IsRegistered)
{
Debug.Console(DebugInfo, this, "Server has already been started");
return;
}
IsRegistered = _server.Register();
Debug.Console(DebugInfo, this, "Starting server, registration {0}", IsRegistered ? "was successful" : "failed");
}
catch (Exception ex)
{
Debug.Console(DebugInfo, this, "Start Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "Start Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "Start Exception InnerException: {0}", ex.InnerException);
}
finally
{
_serverLock.Leave();
}
}
/// <summary>
/// Stop CWS instance
/// </summary>
public void Stop()
{
try
{
_serverLock.Enter();
if (_server == null)
{
Debug.Console(DebugInfo, this, "Server is null or has already been stopped");
return;
}
IsRegistered = _server.Unregister() == false;
Debug.Console(DebugInfo, this, "Stopping server, unregistration {0}", IsRegistered ? "failed" : "was successful");
_server.Dispose();
_server = null;
}
catch (Exception ex)
{
Debug.Console(DebugInfo, this, "Server Stop Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "Server Stop Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "Server Stop Exception InnerException: {0}", ex.InnerException);
}
finally
{
_serverLock.Leave();
}
}
/// <summary>
/// Received request handler
/// </summary>
/// <remarks>
/// This is here for development and testing
/// </remarks>
/// <param name="sender"></param>
/// <param name="args"></param>
public void ReceivedRequestEventHandler(object sender, HttpCwsRequestEventArgs args)
{
try
{
var j = JsonConvert.SerializeObject(args.Context, Formatting.Indented);
Debug.Console(DebugVerbose, this, "RecieveRequestEventHandler Context:\x0d\x0a{0}", j);
}
catch (Exception ex)
{
Debug.Console(DebugInfo, this, "ReceivedRequestEventHandler Exception Message: {0}", ex.Message);
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception StackTrace: {0}", ex.StackTrace);
if (ex.InnerException != null)
Debug.Console(DebugVerbose, this, "ReceivedRequestEventHandler Exception InnerException: {0}", ex.InnerException);
}
}
}
}

View File

@@ -0,0 +1,87 @@
using System;
namespace PepperDash.Core.WebApi.Presets
{
/// <summary>
/// Represents a preset
/// </summary>
public class Preset
{
/// <summary>
/// ID of preset
/// </summary>
public int Id { get; set; }
/// <summary>
/// User ID
/// </summary>
public int UserId { get; set; }
/// <summary>
/// Room Type ID
/// </summary>
public int RoomTypeId { get; set; }
/// <summary>
/// Preset Name
/// </summary>
public string PresetName { get; set; }
/// <summary>
/// Preset Number
/// </summary>
public int PresetNumber { get; set; }
/// <summary>
/// Preset Data
/// </summary>
public string Data { get; set; }
/// <summary>
/// Constructor
/// </summary>
public Preset()
{
PresetName = "";
PresetNumber = 1;
Data = "{}";
}
}
/// <summary>
///
/// </summary>
public class PresetReceivedEventArgs : EventArgs
{
/// <summary>
/// True when the preset is found
/// </summary>
public bool LookupSuccess { get; private set; }
/// <summary>
/// S+ helper
/// </summary>
public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
/// <summary>
/// The preset
/// </summary>
public Preset Preset { get; private set; }
/// <summary>
/// For Simpl+
/// </summary>
public PresetReceivedEventArgs() { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="preset"></param>
/// <param name="success"></param>
public PresetReceivedEventArgs(Preset preset, bool success)
{
LookupSuccess = success;
Preset = preset;
}
}
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Crestron.SimplSharp;
namespace PepperDash.Core.WebApi.Presets
{
/// <summary>
///
/// </summary>
public class User
{
/// <summary>
///
/// </summary>
public int Id { get; set; }
/// <summary>
///
/// </summary>
public string ExternalId { get; set; }
/// <summary>
///
/// </summary>
public string FirstName { get; set; }
/// <summary>
///
/// </summary>
public string LastName { get; set; }
}
/// <summary>
///
/// </summary>
public class UserReceivedEventArgs : EventArgs
{
/// <summary>
/// True when user is found
/// </summary>
public bool LookupSuccess { get; private set; }
/// <summary>
/// For stupid S+
/// </summary>
public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
/// <summary>
///
/// </summary>
public User User { get; private set; }
/// <summary>
/// For Simpl+
/// </summary>
public UserReceivedEventArgs() { }
/// <summary>
/// Constructor
/// </summary>
/// <param name="user"></param>
/// <param name="success"></param>
public UserReceivedEventArgs(User user, bool success)
{
LookupSuccess = success;
User = user;
}
}
/// <summary>
///
/// </summary>
public class UserAndRoomMessage
{
/// <summary>
///
/// </summary>
public int UserId { get; set; }
/// <summary>
///
/// </summary>
public int RoomTypeId { get; set; }
/// <summary>
///
/// </summary>
public int PresetNumber { get; set; }
}
}

View File

@@ -0,0 +1,273 @@
using System;
using Crestron.SimplSharp; // For Basic SIMPL# Classes
using Crestron.SimplSharp.CrestronIO;
using Crestron.SimplSharp.Net.Http;
using Crestron.SimplSharp.Net.Https;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PepperDash.Core.JsonToSimpl;
namespace PepperDash.Core.WebApi.Presets
{
/// <summary>
/// Passcode client for the WebApi
/// </summary>
public class WebApiPasscodeClient : IKeyed
{
/// <summary>
/// Notifies when user received
/// </summary>
public event EventHandler<UserReceivedEventArgs> UserReceived;
/// <summary>
/// Notifies when Preset received
/// </summary>
public event EventHandler<PresetReceivedEventArgs> PresetReceived;
/// <summary>
/// Unique identifier for this instance
/// </summary>
public string Key { get; private set; }
//string JsonMasterKey;
/// <summary>
/// An embedded JsonToSimpl master object.
/// </summary>
JsonToSimplGenericMaster J2SMaster;
string UrlBase;
string DefaultPresetJsonFilePath;
User CurrentUser;
Preset CurrentPreset;
/// <summary>
/// SIMPL+ can only execute the default constructor. If you have variables that require initialization, please
/// use an Initialize method
/// </summary>
public WebApiPasscodeClient()
{
}
/// <summary>
/// Initializes the instance
/// </summary>
/// <param name="key"></param>
/// <param name="jsonMasterKey"></param>
/// <param name="urlBase"></param>
/// <param name="defaultPresetJsonFilePath"></param>
public void Initialize(string key, string jsonMasterKey, string urlBase, string defaultPresetJsonFilePath)
{
Key = key;
//JsonMasterKey = jsonMasterKey;
UrlBase = urlBase;
DefaultPresetJsonFilePath = defaultPresetJsonFilePath;
J2SMaster = new JsonToSimplGenericMaster();
J2SMaster.SaveCallback = this.SaveCallback;
J2SMaster.Initialize(jsonMasterKey);
}
/// <summary>
/// Gets the user for a passcode
/// </summary>
/// <param name="passcode"></param>
public void GetUserForPasscode(string passcode)
{
// Bullshit duplicate code here... These two cases should be the same
// except for https/http and the certificate ignores
if (!UrlBase.StartsWith("https"))
return;
var req = new HttpsClientRequest();
req.Url = new UrlParser(UrlBase + "/api/users/dopin");
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
var jo = new JObject();
jo.Add("pin", passcode);
req.ContentString = jo.ToString();
var client = new HttpsClient();
client.HostVerification = false;
client.PeerVerification = false;
var resp = client.Dispatch(req);
var handler = UserReceived;
if (resp.Code == 200)
{
//CrestronConsole.PrintLine("Received: {0}", resp.ContentString);
var user = JsonConvert.DeserializeObject<User>(resp.ContentString);
CurrentUser = user;
if (handler != null)
UserReceived(this, new UserReceivedEventArgs(user, true));
}
else
if (handler != null)
UserReceived(this, new UserReceivedEventArgs(null, false));
}
/// <summary>
///
/// </summary>
/// <param name="roomTypeId"></param>
/// <param name="presetNumber"></param>
public void GetPresetForThisUser(int roomTypeId, int presetNumber)
{
if (CurrentUser == null)
{
CrestronConsole.PrintLine("GetPresetForThisUser no user loaded");
return;
}
var msg = new UserAndRoomMessage
{
UserId = CurrentUser.Id,
RoomTypeId = roomTypeId,
PresetNumber = presetNumber
};
var handler = PresetReceived;
try
{
if (!UrlBase.StartsWith("https"))
return;
var req = new HttpsClientRequest();
req.Url = new UrlParser(UrlBase + "/api/presets/userandroom");
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
req.ContentString = JsonConvert.SerializeObject(msg);
var client = new HttpsClient();
client.HostVerification = false;
client.PeerVerification = false;
// ask for the preset
var resp = client.Dispatch(req);
if (resp.Code == 200) // got it
{
//Debug.Console(1, this, "Received: {0}", resp.ContentString);
var preset = JsonConvert.DeserializeObject<Preset>(resp.ContentString);
CurrentPreset = preset;
//if there's no preset data, load the template
if (preset.Data == null || preset.Data.Trim() == string.Empty || JObject.Parse(preset.Data).Count == 0)
{
//Debug.Console(1, this, "Loaded preset has no data. Loading default template.");
LoadDefaultPresetData();
return;
}
J2SMaster.LoadWithJson(preset.Data);
if (handler != null)
PresetReceived(this, new PresetReceivedEventArgs(preset, true));
}
else // no existing preset
{
CurrentPreset = new Preset();
LoadDefaultPresetData();
if (handler != null)
PresetReceived(this, new PresetReceivedEventArgs(null, false));
}
}
catch (HttpException e)
{
var resp = e.Response;
Debug.Console(1, this, "No preset received (code {0}). Loading default template", resp.Code);
LoadDefaultPresetData();
if (handler != null)
PresetReceived(this, new PresetReceivedEventArgs(null, false));
}
}
void LoadDefaultPresetData()
{
CurrentPreset = null;
if (!File.Exists(DefaultPresetJsonFilePath))
{
Debug.Console(0, this, "Cannot load default preset file. Saving will not work");
return;
}
using (StreamReader sr = new StreamReader(DefaultPresetJsonFilePath))
{
try
{
var data = sr.ReadToEnd();
J2SMaster.SetJsonWithoutEvaluating(data);
CurrentPreset = new Preset() { Data = data, UserId = CurrentUser.Id };
}
catch (Exception e)
{
Debug.Console(0, this, "Error reading default preset JSON: \r{0}", e);
}
}
}
/// <summary>
///
/// </summary>
/// <param name="roomTypeId"></param>
/// <param name="presetNumber"></param>
public void SavePresetForThisUser(int roomTypeId, int presetNumber)
{
if (CurrentPreset == null)
LoadDefaultPresetData();
//return;
//// A new preset needs to have its numbers set
//if (CurrentPreset.IsNewPreset)
//{
CurrentPreset.UserId = CurrentUser.Id;
CurrentPreset.RoomTypeId = roomTypeId;
CurrentPreset.PresetNumber = presetNumber;
//}
J2SMaster.Save(); // Will trigger callback when ready
}
/// <summary>
/// After save operation on JSON master happens, send it to server
/// </summary>
/// <param name="json"></param>
void SaveCallback(string json)
{
CurrentPreset.Data = json;
if (!UrlBase.StartsWith("https"))
return;
var req = new HttpsClientRequest();
req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
req.Url = new UrlParser(string.Format("{0}/api/presets/addorchange", UrlBase));
req.Header.AddHeader(new HttpsHeader("Content-Type", "application/json"));
req.Header.AddHeader(new HttpsHeader("Accept", "application/json"));
req.ContentString = JsonConvert.SerializeObject(CurrentPreset);
var client = new HttpsClient();
client.HostVerification = false;
client.PeerVerification = false;
try
{
var resp = client.Dispatch(req);
// 201=created
// 204=empty content
if (resp.Code == 201)
CrestronConsole.PrintLine("Preset added");
else if (resp.Code == 204)
CrestronConsole.PrintLine("Preset updated");
else if (resp.Code == 209)
CrestronConsole.PrintLine("Preset already exists. Cannot save as new.");
else
CrestronConsole.PrintLine("Preset save failed: {0}\r", resp.Code, resp.ContentString);
}
catch (HttpException e)
{
CrestronConsole.PrintLine("Preset save exception {0}", e.Response.Code);
}
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Collections.Generic;
using PepperDash.Core.Intersystem.Tokens;
namespace PepperDash.Core.Intersystem.Serialization
{
/// <summary>
/// Interface to determine XSig serialization for an object.
/// </summary>
public interface IXSigSerialization
{
/// <summary>
/// Serialize the sig data
/// </summary>
/// <returns></returns>
IEnumerable<XSigToken> Serialize();
/// <summary>
/// Deserialize the sig data
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="tokens"></param>
/// <returns></returns>
T Deserialize<T>(IEnumerable<XSigToken> tokens) where T : class, IXSigSerialization;
}
}

View File

@@ -0,0 +1,28 @@
using System;
namespace PepperDash.Core.Intersystem.Serialization
{
/// <summary>
/// Class to handle this specific exception type
/// </summary>
public class XSigSerializationException : Exception
{
/// <summary>
/// default constructor
/// </summary>
public XSigSerializationException() { }
/// <summary>
/// constructor with message
/// </summary>
/// <param name="message"></param>
public XSigSerializationException(string message) : base(message) { }
/// <summary>
/// constructor with message and innner exception
/// </summary>
/// <param name="message"></param>
/// <param name="inner"></param>
public XSigSerializationException(string message, Exception inner) : base(message, inner) { }
}
}

View File

@@ -0,0 +1,88 @@
using System;
namespace PepperDash.Core.Intersystem.Tokens
{
/// <summary>
/// Represents an XSigAnalogToken
/// </summary>
public sealed class XSigAnalogToken : XSigToken, IFormattable
{
private readonly ushort _value;
/// <summary>
/// Constructor
/// </summary>
/// <param name="index"></param>
/// <param name="value"></param>
public XSigAnalogToken(int index, ushort value)
: base(index)
{
// 10-bits available for analog encoded data
if (index >= 1024 || index < 0)
throw new ArgumentOutOfRangeException("index");
_value = value;
}
/// <summary>
///
/// </summary>
public ushort Value
{
get { return _value; }
}
/// <summary>
///
/// </summary>
public override XSigTokenType TokenType
{
get { return XSigTokenType.Analog; }
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override byte[] GetBytes()
{
return new[] {
(byte)(0xC0 | ((Value & 0xC000) >> 10) | (Index - 1 >> 7)),
(byte)((Index - 1) & 0x7F),
(byte)((Value & 0x3F80) >> 7),
(byte)(Value & 0x7F)
};
}
/// <summary>
///
/// </summary>
/// <param name="offset"></param>
/// <returns></returns>
public override XSigToken GetTokenWithOffset(int offset)
{
if (offset == 0) return this;
return new XSigAnalogToken(Index + offset, Value);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Index + " = 0x" + Value.ToString("X4");
}
/// <summary>
///
/// </summary>
/// <param name="format"></param>
/// <param name="formatProvider"></param>
/// <returns></returns>
public string ToString(string format, IFormatProvider formatProvider)
{
return Value.ToString(format, formatProvider);
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
namespace PepperDash.Core.Intersystem.Tokens
{
/// <summary>
/// Represents an XSigDigitalToken
/// </summary>
public sealed class XSigDigitalToken : XSigToken
{
private readonly bool _value;
/// <summary>
///
/// </summary>
/// <param name="index"></param>
/// <param name="value"></param>
public XSigDigitalToken(int index, bool value)
: base(index)
{
// 12-bits available for digital encoded data
if (index >= 4096 || index < 0)
throw new ArgumentOutOfRangeException("index");
_value = value;
}
/// <summary>
///
/// </summary>
public bool Value
{
get { return _value; }
}
/// <summary>
///
/// </summary>
public override XSigTokenType TokenType
{
get { return XSigTokenType.Digital; }
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override byte[] GetBytes()
{
return new[] {
(byte)(0x80 | (Value ? 0 : 0x20) | ((Index - 1) >> 7)),
(byte)((Index - 1) & 0x7F)
};
}
/// <summary>
///
/// </summary>
/// <param name="offset"></param>
/// <returns></returns>
public override XSigToken GetTokenWithOffset(int offset)
{
if (offset == 0) return this;
return new XSigDigitalToken(Index + offset, Value);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Index + " = " + (Value ? "High" : "Low");
}
/// <summary>
///
/// </summary>
/// <param name="formatProvider"></param>
/// <returns></returns>
public string ToString(IFormatProvider formatProvider)
{
return Value.ToString(formatProvider);
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Text;
namespace PepperDash.Core.Intersystem.Tokens
{
/// <summary>
/// Represents an XSigSerialToken
/// </summary>
public sealed class XSigSerialToken : XSigToken
{
private readonly string _value;
/// <summary>
/// Constructor
/// </summary>
/// <param name="index"></param>
/// <param name="value"></param>
public XSigSerialToken(int index, string value)
: base(index)
{
// 10-bits available for serial encoded data
if (index >= 1024 || index < 0)
throw new ArgumentOutOfRangeException("index");
_value = value;
}
/// <summary>
///
/// </summary>
public string Value
{
get { return _value; }
}
/// <summary>
///
/// </summary>
public override XSigTokenType TokenType
{
get { return XSigTokenType.Serial; }
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override byte[] GetBytes()
{
var serialBytes = String.IsNullOrEmpty(Value) ? new byte[0] : Encoding.GetEncoding(28591).GetBytes(Value);
var xsig = new byte[serialBytes.Length + 3];
xsig[0] = (byte)(0xC8 | (Index - 1 >> 7));
xsig[1] = (byte)((Index - 1) & 0x7F);
xsig[xsig.Length - 1] = 0xFF;
Buffer.BlockCopy(serialBytes, 0, xsig, 2, serialBytes.Length);
return xsig;
}
/// <summary>
///
/// </summary>
/// <param name="offset"></param>
/// <returns></returns>
public override XSigToken GetTokenWithOffset(int offset)
{
if (offset == 0) return this;
return new XSigSerialToken(Index + offset, Value);
}
/// <summary>
///
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Index + " = \"" + Value + "\"";
}
}
}

View File

@@ -0,0 +1,45 @@
namespace PepperDash.Core.Intersystem.Tokens
{
/// <summary>
/// Represents the base class for all XSig datatypes.
/// </summary>
public abstract class XSigToken
{
private readonly int _index;
/// <summary>
/// Constructs an XSigToken with the specified index.
/// </summary>
/// <param name="index">Index for the data.</param>
protected XSigToken(int index)
{
_index = index;
}
/// <summary>
/// XSig 1-based index.
/// </summary>
public int Index
{
get { return _index; }
}
/// <summary>
/// XSigToken type.
/// </summary>
public abstract XSigTokenType TokenType { get; }
/// <summary>
/// Generates the XSig bytes for the corresponding token.
/// </summary>
/// <returns>XSig byte array.</returns>
public abstract byte[] GetBytes();
/// <summary>
/// Returns a new token if necessary with an updated index based on the specified offset.
/// </summary>
/// <param name="offset">Offset to adjust the index with.</param>
/// <returns>XSigToken</returns>
public abstract XSigToken GetTokenWithOffset(int offset);
}
}

View File

@@ -0,0 +1,23 @@
namespace PepperDash.Core.Intersystem.Tokens
{
/// <summary>
/// XSig token types.
/// </summary>
public enum XSigTokenType
{
/// <summary>
/// Digital signal datatype.
/// </summary>
Digital,
/// <summary>
/// Analog signal datatype.
/// </summary>
Analog,
/// <summary>
/// Serial signal datatype.
/// </summary>
Serial
}
}

View File

@@ -0,0 +1,239 @@
using System;
using System.Linq;
using Crestron.SimplSharp.CrestronIO;
using PepperDash.Core.Intersystem.Serialization;
using PepperDash.Core.Intersystem.Tokens;
/*
Digital (2 bytes)
10C##### 0####### (mask = 11000000_10000000b -> 0xC080)
Analog (4 bytes)
11aa0### 0####### (mask = 11001000_10000000b -> 0xC880)
0aaaaaaa 0aaaaaaa
Serial (Variable length)
11001### 0####### (mask = 11111000_10000000b -> 0xF880)
dddddddd ........ <- up to 252 bytes of serial data (255 - 3)
11111111 <- denotes end of data
*/
namespace PepperDash.Core.Intersystem
{
/// <summary>
/// Helper methods for creating XSig byte sequences compatible with the Intersystem Communications (ISC) symbol.
/// </summary>
/// <remarks>
/// Indexing is not from the start of each signal type but rather from the beginning of the first defined signal
/// the Intersystem Communications (ISC) symbol.
/// </remarks>
public static class XSigHelpers
{
/// <summary>
/// Forces all outputs to 0.
/// </summary>
/// <returns>Bytes in XSig format for clear outputs trigger.</returns>
public static byte[] ClearOutputs()
{
return new byte[] { 0xFC };
}
/// <summary>
/// Evaluate all inputs and re-transmit any digital, analog, and permanent serail signals not set to 0.
/// </summary>
/// <returns>Bytes in XSig format for send status trigger.</returns>
public static byte[] SendStatus()
{
return new byte[] { 0xFD };
}
/// <summary>
/// Get bytes for an IXSigStateResolver object.
/// </summary>
/// <param name="xSigSerialization">XSig state resolver.</param>
/// <returns>Bytes in XSig format for each token within the state representation.</returns>
public static byte[] GetBytes(IXSigSerialization xSigSerialization)
{
return GetBytes(xSigSerialization, 0);
}
/// <summary>
/// Get bytes for an IXSigStateResolver object, with a specified offset.
/// </summary>
/// <param name="xSigSerialization">XSig state resolver.</param>
/// <param name="offset">Offset to which the data will be aligned.</param>
/// <returns>Bytes in XSig format for each token within the state representation.</returns>
public static byte[] GetBytes(IXSigSerialization xSigSerialization, int offset)
{
var tokens = xSigSerialization.Serialize();
if (tokens == null) return new byte[0];
using (var memoryStream = new MemoryStream())
{
using (var tokenWriter = new XSigTokenStreamWriter(memoryStream))
tokenWriter.WriteXSigData(xSigSerialization, offset);
return memoryStream.ToArray();
}
}
/// <summary>
/// Get bytes for a single digital signal.
/// </summary>
/// <param name="index">1-based digital index</param>
/// <param name="value">Digital data to be encoded</param>
/// <returns>Bytes in XSig format for digtial information.</returns>
public static byte[] GetBytes(int index, bool value)
{
return GetBytes(index, 0, value);
}
/// <summary>
/// Get bytes for a single digital signal.
/// </summary>
/// <param name="index">1-based digital index</param>
/// <param name="offset">Index offset.</param>
/// <param name="value">Digital data to be encoded</param>
/// <returns>Bytes in XSig format for digtial information.</returns>
public static byte[] GetBytes(int index, int offset, bool value)
{
return new XSigDigitalToken(index + offset, value).GetBytes();
}
/// <summary>
/// Get byte sequence for multiple digital signals.
/// </summary>
/// <param name="startIndex">Starting index of the sequence.</param>
/// <param name="values">Digital signal value array.</param>
/// <returns>Byte sequence in XSig format for digital signal information.</returns>
public static byte[] GetBytes(int startIndex, bool[] values)
{
return GetBytes(startIndex, 0, values);
}
/// <summary>
/// Get byte sequence for multiple digital signals.
/// </summary>
/// <param name="startIndex">Starting index of the sequence.</param>
/// <param name="offset">Index offset.</param>
/// <param name="values">Digital signal value array.</param>
/// <returns>Byte sequence in XSig format for digital signal information.</returns>
public static byte[] GetBytes(int startIndex, int offset, bool[] values)
{
// Digital XSig data is 2 bytes per value
const int fixedLength = 2;
var bytes = new byte[values.Length * fixedLength];
for (var i = 0; i < values.Length; i++)
Buffer.BlockCopy(GetBytes(startIndex++, offset, values[i]), 0, bytes, i * fixedLength, fixedLength);
return bytes;
}
/// <summary>
/// Get bytes for a single analog signal.
/// </summary>
/// <param name="index">1-based analog index</param>
/// <param name="value">Analog data to be encoded</param>
/// <returns>Bytes in XSig format for analog signal information.</returns>
public static byte[] GetBytes(int index, ushort value)
{
return GetBytes(index, 0, value);
}
/// <summary>
/// Get bytes for a single analog signal.
/// </summary>
/// <param name="index">1-based analog index</param>
/// <param name="offset">Index offset.</param>
/// <param name="value">Analog data to be encoded</param>
/// <returns>Bytes in XSig format for analog signal information.</returns>
public static byte[] GetBytes(int index, int offset, ushort value)
{
return new XSigAnalogToken(index + offset, value).GetBytes();
}
/// <summary>
/// Get byte sequence for multiple analog signals.
/// </summary>
/// <param name="startIndex">Starting index of the sequence.</param>
/// <param name="values">Analog signal value array.</param>
/// <returns>Byte sequence in XSig format for analog signal information.</returns>
public static byte[] GetBytes(int startIndex, ushort[] values)
{
return GetBytes(startIndex, 0, values);
}
/// <summary>
/// Get byte sequence for multiple analog signals.
/// </summary>
/// <param name="startIndex">Starting index of the sequence.</param>
/// <param name="offset">Index offset.</param>
/// <param name="values">Analog signal value array.</param>
/// <returns>Byte sequence in XSig format for analog signal information.</returns>
public static byte[] GetBytes(int startIndex, int offset, ushort[] values)
{
// Analog XSig data is 4 bytes per value
const int fixedLength = 4;
var bytes = new byte[values.Length * fixedLength];
for (var i = 0; i < values.Length; i++)
Buffer.BlockCopy(GetBytes(startIndex++, offset, values[i]), 0, bytes, i * fixedLength, fixedLength);
return bytes;
}
/// <summary>
/// Get bytes for a single serial signal.
/// </summary>
/// <param name="index">1-based serial index</param>
/// <param name="value">Serial data to be encoded</param>
/// <returns>Bytes in XSig format for serial signal information.</returns>
public static byte[] GetBytes(int index, string value)
{
return GetBytes(index, 0, value);
}
/// <summary>
/// Get bytes for a single serial signal.
/// </summary>
/// <param name="index">1-based serial index</param>
/// <param name="offset">Index offset.</param>
/// <param name="value">Serial data to be encoded</param>
/// <returns>Bytes in XSig format for serial signal information.</returns>
public static byte[] GetBytes(int index, int offset, string value)
{
return new XSigSerialToken(index + offset, value).GetBytes();
}
/// <summary>
/// Get byte sequence for multiple serial signals.
/// </summary>
/// <param name="startIndex">Starting index of the sequence.</param>
/// <param name="values">Serial signal value array.</param>
/// <returns>Byte sequence in XSig format for serial signal information.</returns>
public static byte[] GetBytes(int startIndex, string[] values)
{
return GetBytes(startIndex, 0, values);
}
/// <summary>
/// Get byte sequence for multiple serial signals.
/// </summary>
/// <param name="startIndex">Starting index of the sequence.</param>
/// <param name="offset">Index offset.</param>
/// <param name="values">Serial signal value array.</param>
/// <returns>Byte sequence in XSig format for serial signal information.</returns>
public static byte[] GetBytes(int startIndex, int offset, string[] values)
{
// Serial XSig data is not fixed-length like the other formats
var dstOffset = 0;
var bytes = new byte[values.Sum(v => v.Length + 3)];
for (var i = 0; i < values.Length; i++)
{
var data = GetBytes(startIndex++, offset, values[i]);
Buffer.BlockCopy(data, 0, bytes, dstOffset, data.Length);
dstOffset += data.Length;
}
return bytes;
}
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using Crestron.SimplSharp.CrestronIO;
using PepperDash.Core.Intersystem.Serialization;
using PepperDash.Core.Intersystem.Tokens;
namespace PepperDash.Core.Intersystem
{
/// <summary>
/// XSigToken stream reader.
/// </summary>
public sealed class XSigTokenStreamReader : IDisposable
{
private readonly Stream _stream;
private readonly bool _leaveOpen;
/// <inheritdoc />
/// <summary>
/// XSigToken stream reader constructor.
/// </summary>
/// <param name="stream">Input stream to read from.</param>
/// <exception cref="T:System.ArgumentNullException">Stream is null.</exception>
/// <exception cref="T:System.ArgumentException">Stream cannot be read from.</exception>
public XSigTokenStreamReader(Stream stream)
: this(stream, false) { }
/// <summary>
/// XSigToken stream reader constructor.
/// </summary>
/// <param name="stream">Input stream to read from.</param>
/// <param name="leaveOpen">Determines whether to leave the stream open or not.</param>
/// <exception cref="ArgumentNullException">Stream is null.</exception>
/// <exception cref="ArgumentException">Stream cannot be read from.</exception>
public XSigTokenStreamReader(Stream stream, bool leaveOpen)
{
if (stream == null)
throw new ArgumentNullException("stream");
if (!stream.CanRead)
throw new ArgumentException("The specified stream cannot be read from.");
_stream = stream;
_leaveOpen = leaveOpen;
}
/// <summary>
/// Reads a 16-bit unsigned integer from the specified stream using Big Endian byte order.
/// </summary>
/// <param name="stream">Input stream</param>
/// <param name="value">Result</param>
/// <returns>True if successful, otherwise false.</returns>
public static bool TryReadUInt16BE(Stream stream, out ushort value)
{
value = 0;
if (stream.Length < 2)
return false;
var buffer = new byte[2];
stream.Read(buffer, 0, 2);
value = (ushort)((buffer[0] << 8) | buffer[1]);
return true;
}
/// <summary>
/// Read XSig token from the stream.
/// </summary>
/// <returns>XSigToken</returns>
/// <exception cref="ArgumentOutOfRangeException">Offset is less than 0.</exception>
public XSigToken ReadXSigToken()
{
ushort prefix;
if (!TryReadUInt16BE(_stream, out prefix))
return null;
if ((prefix & 0xF880) == 0xC800) // Serial data
{
var index = ((prefix & 0x0700) >> 1) | (prefix & 0x7F);
var n = 0;
const int maxSerialDataLength = 252;
var chars = new char[maxSerialDataLength];
int ch;
while ((ch = _stream.ReadByte()) != 0xFF)
{
if (ch == -1) // Reached end of stream without end of data marker
return null;
chars[n++] = (char)ch;
}
return new XSigSerialToken((ushort)(index + 1), new string(chars, 0, n));
}
if ((prefix & 0xC880) == 0xC000) // Analog data
{
ushort data;
if (!TryReadUInt16BE(_stream, out data))
return null;
var index = ((prefix & 0x0700) >> 1) | (prefix & 0x7F);
var value = ((prefix & 0x3000) << 2) | ((data & 0x7F00) >> 1) | (data & 0x7F);
return new XSigAnalogToken((ushort)(index + 1), (ushort)value);
}
if ((prefix & 0xC080) == 0x8000) // Digital data
{
var index = ((prefix & 0x1F00) >> 1) | (prefix & 0x7F);
var value = (prefix & 0x2000) == 0;
return new XSigDigitalToken((ushort)(index + 1), value);
}
return null;
}
/// <summary>
/// Reads all available XSig tokens from the stream.
/// </summary>
/// <returns>XSigToken collection.</returns>
public IEnumerable<XSigToken> ReadAllXSigTokens()
{
var tokens = new List<XSigToken>();
XSigToken token;
while ((token = ReadXSigToken()) != null)
tokens.Add(token);
return tokens;
}
/// <summary>
/// Attempts to deserialize all XSig data within the stream from the current position.
/// </summary>
/// <typeparam name="T">Type to deserialize the information to.</typeparam>
/// <returns>Deserialized object.</returns>
public T DeserializeStream<T>()
where T : class, IXSigSerialization, new()
{
return new T().Deserialize<T>(ReadAllXSigTokens());
}
/// <summary>
/// Disposes of the internal stream if specified to not leave open.
/// </summary>
public void Dispose()
{
if (!_leaveOpen)
_stream.Dispose();
}
}
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Linq;
using System.Collections.Generic;
using Crestron.SimplSharp.CrestronIO;
using PepperDash.Core.Intersystem.Serialization;
using PepperDash.Core.Intersystem.Tokens;
namespace PepperDash.Core.Intersystem
{
/// <summary>
/// XSigToken stream writer.
/// </summary>
public sealed class XSigTokenStreamWriter : IDisposable
{
private readonly Stream _stream;
private readonly bool _leaveOpen;
/// <inheritdoc />
/// <summary>
/// XSigToken stream writer constructor.
/// </summary>
/// <param name="stream">Input stream to write to.</param>
/// <exception cref="T:System.ArgumentNullException">Stream is null.</exception>
/// <exception cref="T:System.ArgumentException">Stream cannot be written to.</exception>
public XSigTokenStreamWriter(Stream stream)
: this(stream, false) { }
/// <summary>
/// XSigToken stream writer constructor.
/// </summary>
/// <param name="stream">Input stream to write to.</param>
/// <param name="leaveOpen">Determines whether to leave the stream open or not.</param>
/// <exception cref="ArgumentNullException">Stream is null.</exception>
/// <exception cref="ArgumentException">Stream cannot be written to.</exception>
public XSigTokenStreamWriter(Stream stream, bool leaveOpen)
{
if (stream == null)
throw new ArgumentNullException("stream");
if (!stream.CanWrite)
throw new ArgumentException("The specified stream cannot be written to.");
_stream = stream;
_leaveOpen = leaveOpen;
}
/// <summary>
/// Write XSig data gathered from an IXSigStateResolver to the stream.
/// </summary>
/// <param name="xSigSerialization">IXSigStateResolver object.</param>
public void WriteXSigData(IXSigSerialization xSigSerialization)
{
WriteXSigData(xSigSerialization, 0);
}
/// <summary>
/// Write XSig data gathered from an IXSigStateResolver to the stream.
/// </summary>
/// <param name="xSigSerialization">IXSigStateResolver object.</param>
/// <param name="offset">Index offset for each XSigToken.</param>
public void WriteXSigData(IXSigSerialization xSigSerialization, int offset)
{
if (xSigSerialization == null)
throw new ArgumentNullException("xSigSerialization");
var tokens = xSigSerialization.Serialize();
WriteXSigData(tokens, offset);
}
/// <summary>
/// Write XSigToken to the stream.
/// </summary>
/// <param name="token">XSigToken object.</param>
public void WriteXSigData(XSigToken token)
{
WriteXSigData(token, 0);
}
/// <summary>
/// Write XSigToken to the stream.
/// </summary>
/// <param name="token">XSigToken object.</param>
/// <param name="offset">Index offset for each XSigToken.</param>
public void WriteXSigData(XSigToken token, int offset)
{
WriteXSigData(new[] { token }, offset);
}
/// <summary>
/// Writes an array of XSigTokens to the stream.
/// </summary>
/// <param name="tokens">XSigToken objects.</param>
public void WriteXSigData(XSigToken[] tokens)
{
WriteXSigData(tokens.AsEnumerable());
}
/// <summary>
/// Write an enumerable collection of XSigTokens to the stream.
/// </summary>
/// <param name="tokens">XSigToken objects.</param>
public void WriteXSigData(IEnumerable<XSigToken> tokens)
{
WriteXSigData(tokens, 0);
}
/// <summary>
/// Write an enumerable collection of XSigTokens to the stream.
/// </summary>
/// <param name="tokens">XSigToken objects.</param>
/// <param name="offset">Index offset for each XSigToken.</param>
public void WriteXSigData(IEnumerable<XSigToken> tokens, int offset)
{
if (offset < 0)
throw new ArgumentOutOfRangeException("offset", "Offset must be greater than or equal to 0.");
if (tokens != null)
{
foreach (var token in tokens)
{
if (token == null) continue;
var bytes = token.GetTokenWithOffset(offset).GetBytes();
_stream.Write(bytes, 0, bytes.Length);
}
}
}
/// <summary>
/// Disposes of the internal stream if specified to not leave open.
/// </summary>
public void Dispose()
{
if (!_leaveOpen)
_stream.Dispose();
}
}
}

View File

@@ -25,7 +25,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="PepperDashCore" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
@@ -33,4 +32,7 @@
<ItemGroup>
<Folder Include="Display\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -24,10 +24,10 @@
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="PepperDashCore" Version="2.0.1" />
</ItemGroup>
</Project>

View File

@@ -33,9 +33,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="PepperDashCore" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>

View File

@@ -38,7 +38,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
<PackageReference Include="PepperDashCore" Version="2.0.1" />
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
@@ -53,6 +52,7 @@
<None Remove="C:\Users\awelker\source\Essentials\Essentials\src\PepperDash.Essentials.MobileControl\bin\Debug\net472\PepperDash.Essentials.MobileControl.4.0.0-local.net472.cplz" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj">
<Private>false</Private>
<ExcludeAssets>runtime</ExcludeAssets>

View File

@@ -48,9 +48,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" />
<PackageReference Include="PepperDashCore" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
<ProjectReference Include="..\PepperDash.Essentials.Devices.Common\PepperDash.Essentials.Devices.Common.csproj" />
<ProjectReference Include="..\PepperDash.Essentials.MobileControl.Messengers\PepperDash.Essentials.MobileControl.Messengers.csproj" />