diff --git a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml
index 99b67d92..291e9371 100644
--- a/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml
+++ b/.github/workflows/EssentialsPlugins-builds-4-series-caller.yml
@@ -18,4 +18,5 @@ jobs:
newVersion: ${{ needs.getVersion.outputs.newVersion }}
version: ${{ needs.getVersion.outputs.version }}
tag: ${{ needs.getVersion.outputs.tag }}
- channel: ${{ needs.getVersion.outputs.channel }}
\ No newline at end of file
+ channel: ${{ needs.getVersion.outputs.channel }}
+ bypassPackageCheck: true
\ No newline at end of file
diff --git a/PepperDash.Essentials.4Series.sln b/PepperDash.Essentials.4Series.sln
index e2db852a..7423c50a 100644
--- a/PepperDash.Essentials.4Series.sln
+++ b/PepperDash.Essentials.4Series.sln
@@ -4,10 +4,37 @@ 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
@@ -34,10 +61,36 @@ Global
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
+ {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
+ {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F6D362DE-2256-44B1-927A-8CE4705D839A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F6D362DE-2256-44B1-927A-8CE4705D839A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU
+ {B438694F-8FF7-464A-9EC8-10427374471F}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU
+ {B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {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
EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {53E204B7-97DD-441D-A96C-721DF014DF82} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
+ {CB3B11BA-625C-4D35-B663-FDC5BE9A230E} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA}
+ {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}
EndGlobalSection
diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/CenIoCom/CenIoComController.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/CenIoCom/CenIoComController.cs
deleted file mode 100644
index bbd496b4..00000000
--- a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/CenIoCom/CenIoComController.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using System.Collections.Generic;
-using Crestron.SimplSharpPro;
-using Crestron.SimplSharpPro.GeneralIO;
-using PepperDash.Core;
-using PepperDash.Essentials.Core.Config;
-
-namespace PepperDash.Essentials.Core.CrestronIO
-{
- ///
- /// Wrapper class for CEN-IO-COM-Xxx expander module
- ///
- [Description("Wrapper class for the CEN-IO-COM-102 & CEN-IO-COM-202 expander module")]
- public class CenIoComController : CrestronGenericBaseDevice, IComPorts
- {
- private readonly CenIoCom _cenIoCom;
-
- public CenIoComController(string key, string name, CenIoCom cenIo)
- :base(key, name, cenIo)
- {
- _cenIoCom = cenIo;
- }
-
- #region Implementation of IComPorts
-
- public CrestronCollection ComPorts
- {
- get { return _cenIoCom.ComPorts; }
- }
-
- public int NumberOfComPorts
- {
- get { return _cenIoCom.NumberOfComPorts; }
- }
-
- #endregion
-
- }
-
- public class CenIoCom102ControllerFactory : EssentialsDeviceFactory
- {
- private const string CenIoCom102Type = "ceniocom102";
- private const string CenIoCom202Type = "ceniocom202";
-
- public CenIoCom102ControllerFactory()
- {
- TypeNames = new List { CenIoCom102Type, CenIoCom202Type };
- }
-
- public override EssentialsDevice BuildDevice(DeviceConfig dc)
- {
- Debug.Console(1, "Factory Attempting to create new CEN-IO-COM-Xxx Device");
-
- var control = CommFactory.GetControlPropertiesConfig(dc);
- if (control == null)
- {
- Debug.Console(1, "Factory failed to create a new CEN-IO-COM-Xxx Device, control properties not found");
- return null;
- }
-
- var ipid = control.IpIdInt;
- if (ipid < 2)
- {
- Debug.Console(1, "Factory failed to create a new CEN-IO-COM-Xxx Device, invalid IP-ID found");
- return null;
- }
-
- switch (dc.Type)
- {
- case CenIoCom102Type:
- {
- return new CenIoComController(dc.Key, dc.Name, new CenIoCom102(ipid, Global.ControlSystem));
- }
- case CenIoCom202Type:
- {
- return new CenIoComController(dc.Key, dc.Name, new CenIoCom202(ipid, Global.ControlSystem));
- }
- default:
- {
- Debug.Console(1, "Factory failed to create a new CEN-IO-COM-Xxx Device, invalid type '{0}'", dc.Type);
- return null;
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/HdPsXxxAnalogAuxMixerController.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/HdPsXxxAnalogAuxMixerController.cs
deleted file mode 100644
index 7d27d35b..00000000
--- a/essentials-framework/Essentials DM/Essentials_DM/Chassis/HdPsXxxAnalogAuxMixerController.cs
+++ /dev/null
@@ -1,185 +0,0 @@
-using Crestron.SimplSharp;
-using Crestron.SimplSharpPro.DeviceSupport;
-using Crestron.SimplSharpPro.DM;
-using PepperDash.Core;
-using PepperDash.Essentials.Core;
-
-namespace PepperDash_Essentials_DM.Chassis
-{
- public class HdPsXxxAnalogAuxMixerController : IKeyed,
- IHasVolumeControlWithFeedback, IHasMuteControlWithFeedback
- {
- public string Key { get; private set; }
-
- private readonly HdPsXxxAnalogAuxMixer _mixer;
-
- public HdPsXxxAnalogAuxMixerController(string parent, uint mixer, HdPsXxx chassis)
- {
- Key = string.Format("{0}-analogMixer{1}", parent, mixer);
-
- _mixer = chassis.AnalogAuxiliaryMixer[mixer];
-
- _mixer.AuxMixerPropertyChange += OnAuxMixerPropertyChange;
- _mixer.AuxiliaryMuteControl.MuteAndVolumeControlPropertyChange += OnMuteAndVolumeControlPropertyChange;
-
- VolumeLevelFeedback = new IntFeedback(() => VolumeLevel);
- MuteFeedback = new BoolFeedback(() => IsMuted);
- }
-
- #region Volume
-
- private void OnAuxMixerPropertyChange(object sender, GenericEventArgs args)
- {
- Debug.Console(2, this, "OnAuxMixerPropertyChange: {0} > Index-{1}, EventId-{2}", sender.ToString(), args.Index, args.EventId);
-
- switch (args.EventId)
- {
- case MuteAndVolumeContorlEventIds.VolumeFeedbackEventId:
- {
- VolumeLevel = _mixer.VolumeFeedback.ShortValue;
- break;
- }
- case MuteAndVolumeContorlEventIds.MuteOnEventId:
- case MuteAndVolumeContorlEventIds.MuteOffEventId:
- {
- IsMuted = _mixer.AuxiliaryMuteControl.MuteOnFeedback.BoolValue;
- break;
- }
- default:
- {
- Debug.Console(1, this, "OnAuxMixerPropertyChange: {0} > Index-{1}, EventId-{2} - unhandled eventId", sender.ToString(), args.Index, args.EventId);
- break;
- }
- }
- }
-
- private const ushort CrestronLevelMin = 0;
- private const ushort CrestronLevelMax = 65535;
-
- private const int DeviceLevelMin = -800;
- private const int DeviceLevelMax = 200;
-
- private const int RampTime = 5000;
-
- private int _volumeLevel;
-
- public int VolumeLevel
- {
- get { return _volumeLevel; }
- private set
- {
- var level = value;
-
- _volumeLevel = CrestronEnvironment.ScaleWithLimits(level, DeviceLevelMax, DeviceLevelMin, CrestronLevelMax, CrestronLevelMin);
-
- Debug.Console(1, this, "VolumeFeedback: level-'{0}', scaled-'{1}'", level, _volumeLevel);
-
- VolumeLevelFeedback.FireUpdate();
- }
- }
-
- public IntFeedback VolumeLevelFeedback { get; private set; }
-
- public void SetVolume(ushort level)
- {
- var levelScaled = CrestronEnvironment.ScaleWithLimits(level, CrestronLevelMax, CrestronLevelMin, DeviceLevelMax, DeviceLevelMin);
-
- Debug.Console(1, this, "SetVolume: level-'{0}', levelScaled-'{1}'", level, levelScaled);
-
- _mixer.Volume.ShortValue = (short)levelScaled;
- }
-
- public void VolumeUp(bool pressRelease)
- {
- if (pressRelease)
- {
- _mixer.Volume.CreateSignedRamp(DeviceLevelMax, RampTime);
- }
- else
- {
- _mixer.Volume.StopRamp();
- }
- }
-
- public void VolumeDown(bool pressRelease)
- {
- if (pressRelease)
- {
- _mixer.Volume.CreateSignedRamp(DeviceLevelMin, RampTime);
- }
- else
- {
- _mixer.Volume.StopRamp();
- }
- }
-
- #endregion
-
-
-
-
- #region Mute
-
- private void OnMuteAndVolumeControlPropertyChange(MuteControl device, GenericEventArgs args)
- {
- Debug.Console(2, this, "OnMuteAndVolumeControlPropertyChange: {0} > Index-{1}, EventId-{2}", device.ToString(), args.Index, args.EventId);
-
- switch (args.EventId)
- {
- case MuteAndVolumeContorlEventIds.VolumeFeedbackEventId:
- {
- VolumeLevel = _mixer.VolumeFeedback.ShortValue;
- break;
- }
- case MuteAndVolumeContorlEventIds.MuteOnEventId:
- case MuteAndVolumeContorlEventIds.MuteOffEventId:
- {
- IsMuted = _mixer.AuxiliaryMuteControl.MuteOnFeedback.BoolValue;
- break;
- }
- default:
- {
- Debug.Console(1, this, "OnMuteAndVolumeControlPropertyChange: {0} > Index-{1}, EventId-{2} - unhandled eventId", device.ToString(), args.Index, args.EventId);
- break;
- }
- }
- }
-
- private bool _isMuted;
-
- public bool IsMuted
- {
- get { return _isMuted; }
- set
- {
- _isMuted = value;
-
- Debug.Console(1, this, "IsMuted: _isMuted-'{0}'", _isMuted);
-
- MuteFeedback.FireUpdate();
- }
- }
-
- public BoolFeedback MuteFeedback { get; private set; }
-
- public void MuteOn()
- {
- _mixer.AuxiliaryMuteControl.MuteOn();
- }
-
- public void MuteOff()
- {
- _mixer.AuxiliaryMuteControl.MuteOff();
- }
-
- public void MuteToggle()
- {
- if (IsMuted)
- MuteOff();
- else
- MuteOn();
- }
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/essentials-framework/Essentials DM/Essentials_DM/Chassis/HdPsXxxOutputAudioController.cs b/essentials-framework/Essentials DM/Essentials_DM/Chassis/HdPsXxxOutputAudioController.cs
deleted file mode 100644
index 57067bde..00000000
--- a/essentials-framework/Essentials DM/Essentials_DM/Chassis/HdPsXxxOutputAudioController.cs
+++ /dev/null
@@ -1,167 +0,0 @@
-using Crestron.SimplSharp;
-using Crestron.SimplSharpPro.DM;
-using PepperDash.Core;
-using PepperDash.Essentials.Core;
-
-namespace PepperDash_Essentials_DM.Chassis
-{
- public class HdPsXxxOutputAudioController : IKeyed,
- IHasVolumeControlWithFeedback, IHasMuteControlWithFeedback
- {
- public string Key { get; private set; }
-
- private readonly HdPsXxxHdmiDmLiteOutputMixer _mixer; // volume/volumeFeedback
- private readonly HdPsXxxOutputPort _port; // mute/muteFeedback
-
- public HdPsXxxOutputAudioController(string parent, uint output, HdPsXxx chassis)
- {
- Key = string.Format("{0}-audioOut{1}", parent, output);
-
- _port = chassis.HdmiDmLiteOutputs[output].OutputPort;
- _mixer = chassis.HdmiDmLiteOutputs[output].Mixer;
-
- chassis.DMOutputChange += ChassisOnDmOutputChange;
-
- VolumeLevelFeedback = new IntFeedback(() => VolumeLevel);
- MuteFeedback = new BoolFeedback(() => IsMuted);
- }
-
- private void ChassisOnDmOutputChange(Switch device, DMOutputEventArgs args)
- {
- switch (args.EventId)
- {
- case (DMOutputEventIds.VolumeEventId):
- {
- Debug.Console(2, this, "HdPsXxxOutputAudioController: {0} > Index-{1}, Number-{3}, EventId-{2} - AudioMute/UnmuteEventId",
- device.ToString(), args.Index, args.EventId, args.Number);
-
- VolumeLevel = _mixer.VolumeFeedback.ShortValue;
-
- break;
- }
- case DMOutputEventIds.MuteOnEventId:
- case DMOutputEventIds.MuteOffEventId:
- {
- Debug.Console(2, this, "HdPsXxxOutputAudioController: {0} > Index-{1}, Number-{3}, EventId-{2} - MuteOnEventId/MuteOffEventId",
- device.ToString(), args.Index, args.EventId, args.Number);
-
- IsMuted = _port.MuteOnFeedback.BoolValue;
-
- break;
- }
- default:
- {
- Debug.Console(1, this, "HdPsXxxOutputAudioController: {0} > Index-{1}, Number-{3}, EventId-{2} - unhandled eventId",
- device.ToString(), args.Index, args.EventId, args.Number);
- break;
- }
- }
- }
-
- #region Volume
-
- private const ushort CrestronLevelMin = 0;
- private const ushort CrestronLevelMax = 65535;
-
- private const int DeviceLevelMin = -800;
- private const int DeviceLevelMax = 200;
-
- private const int RampTime = 5000;
-
- private int _volumeLevel;
-
- public int VolumeLevel
- {
- get { return _volumeLevel; }
- private set
- {
- var level = value;
-
- _volumeLevel = CrestronEnvironment.ScaleWithLimits(level, DeviceLevelMax, DeviceLevelMin, CrestronLevelMax, CrestronLevelMin);
-
- Debug.Console(2, this, "VolumeFeedback: level-'{0}', scaled-'{1}'", level, _volumeLevel);
-
- VolumeLevelFeedback.FireUpdate();
- }
- }
-
- public IntFeedback VolumeLevelFeedback { get; private set; }
-
- public void SetVolume(ushort level)
- {
- var levelScaled = CrestronEnvironment.ScaleWithLimits(level, CrestronLevelMax, CrestronLevelMin, DeviceLevelMax, DeviceLevelMin);
-
- Debug.Console(1, this, "SetVolume: level-'{0}', levelScaled-'{1}'", level, levelScaled);
-
- _mixer.Volume.ShortValue = (short)levelScaled;
- }
-
- public void VolumeUp(bool pressRelease)
- {
- if (pressRelease)
- {
- _mixer.Volume.CreateSignedRamp(DeviceLevelMax, RampTime);
- }
- else
- {
- _mixer.Volume.StopRamp();
- }
- }
-
- public void VolumeDown(bool pressRelease)
- {
- if (pressRelease)
- {
- _mixer.Volume.CreateSignedRamp(DeviceLevelMin, RampTime);
- }
- else
- {
- _mixer.Volume.StopRamp();
- }
- }
-
- #endregion
-
-
-
-
- #region Mute
-
- private bool _isMuted;
-
- public bool IsMuted
- {
- get { return _isMuted; }
- set
- {
- _isMuted = value;
-
- Debug.Console(1, this, "IsMuted: _isMuted-'{0}'", _isMuted);
-
- MuteFeedback.FireUpdate();
- }
- }
-
- public BoolFeedback MuteFeedback { get; private set; }
-
- public void MuteOn()
- {
- _port.MuteOn();
- }
-
- public void MuteOff()
- {
- _port.MuteOff();
- }
-
- public void MuteToggle()
- {
- if (IsMuted)
- MuteOff();
- else
- MuteOn();
- }
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index 9c82b403..a8a95eda 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -2,10 +2,10 @@
2.0.0-local
$(Version)
- PepperDash Technologies
- PepperDash Technologies
+ PepperDash Technology
+ PepperDash Technology
PepperDash Essentials
- Copyright © 2023
+ Copyright © 2025
https://github.com/PepperDash/Essentials
git
Crestron; 4series
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
index 505fad59..93647372 100644
--- a/src/Directory.Build.targets
+++ b/src/Directory.Build.targets
@@ -33,6 +33,11 @@
+
+
+
+
+
diff --git a/src/PepperDash.Core/Comm/CommunicationGather.cs b/src/PepperDash.Core/Comm/CommunicationGather.cs
new file mode 100644
index 00000000..9ffe8262
--- /dev/null
+++ b/src/PepperDash.Core/Comm/CommunicationGather.cs
@@ -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
+{
+ ///
+ /// Defines the string event handler for line events on the gather
+ ///
+ ///
+ public delegate void LineReceivedHandler(string text);
+
+ ///
+ /// Attaches to IBasicCommunication as a text gather
+ ///
+ public class CommunicationGather
+ {
+ ///
+ /// Event that fires when a line is received from the IBasicCommunication source.
+ /// The event merely contains the text, not an EventArgs type class.
+ ///
+ public event EventHandler LineReceived;
+
+ ///
+ /// The communication port that this gathers on
+ ///
+ public ICommunicationReceiver Port { get; private set; }
+
+ ///
+ /// Default false. If true, the delimiter will be included in the line output
+ /// events
+ ///
+ public bool IncludeDelimiter { get; set; }
+
+ ///
+ /// For receive buffer
+ ///
+ StringBuilder ReceiveBuffer = new StringBuilder();
+
+ ///
+ /// Delimiter, like it says!
+ ///
+ char Delimiter;
+
+ string[] StringDelimiters;
+
+ ///
+ /// Constructor for using a char delimiter
+ ///
+ ///
+ ///
+ public CommunicationGather(ICommunicationReceiver port, char delimiter)
+ {
+ Port = port;
+ Delimiter = delimiter;
+ port.TextReceived += new EventHandler(Port_TextReceived);
+ }
+
+ ///
+ /// Constructor for using a single string delimiter
+ ///
+ ///
+ ///
+ public CommunicationGather(ICommunicationReceiver port, string delimiter)
+ :this(port, new string[] { delimiter} )
+ {
+ }
+
+ ///
+ /// Constructor for using an array of string delimiters
+ ///
+ ///
+ ///
+ public CommunicationGather(ICommunicationReceiver port, string[] delimiters)
+ {
+ Port = port;
+ StringDelimiters = delimiters;
+ port.TextReceived += Port_TextReceivedStringDelimiter;
+ }
+
+ ///
+ /// Disconnects this gather from the Port's TextReceived event. This will not fire LineReceived
+ /// after the this call.
+ ///
+ public void Stop()
+ {
+ Port.TextReceived -= Port_TextReceived;
+ Port.TextReceived -= Port_TextReceivedStringDelimiter;
+ }
+
+ ///
+ /// Handler for raw data coming from port
+ ///
+ 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]);
+ }
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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]);
+ }
+ }
+ }
+
+ ///
+ /// Deconstructor. Disconnects from port TextReceived events.
+ ///
+ ~CommunicationGather()
+ {
+ Stop();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs
new file mode 100644
index 00000000..0a38d826
--- /dev/null
+++ b/src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs
@@ -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
+{
+ ///
+ /// Controls the ability to disable/enable debugging of TX/RX data sent to/from a device with a built in timer to disable
+ ///
+ public class CommunicationStreamDebugging
+ {
+ ///
+ /// Device Key that this instance configures
+ ///
+ public string ParentDeviceKey { get; private set; }
+
+ ///
+ /// Timer to disable automatically if not manually disabled
+ ///
+ private CTimer DebugExpiryPeriod;
+
+ ///
+ /// The current debug setting
+ ///
+ public eStreamDebuggingSetting DebugSetting { get; private set; }
+
+ private uint _DebugTimeoutInMs;
+ private const uint _DefaultDebugTimeoutMin = 30;
+
+ ///
+ /// Timeout in Minutes
+ ///
+ public uint DebugTimeoutMinutes
+ {
+ get
+ {
+ return _DebugTimeoutInMs/60000;
+ }
+ }
+
+ ///
+ /// Indicates that receive stream debugging is enabled
+ ///
+ public bool RxStreamDebuggingIsEnabled{ get; private set; }
+
+ ///
+ /// Indicates that transmit stream debugging is enabled
+ ///
+ public bool TxStreamDebuggingIsEnabled { get; private set; }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ public CommunicationStreamDebugging(string parentDeviceKey)
+ {
+ ParentDeviceKey = parentDeviceKey;
+ }
+
+
+ ///
+ /// Sets the debugging setting and if not setting to off, assumes the default of 30 mintues
+ ///
+ ///
+ public void SetDebuggingWithDefaultTimeout(eStreamDebuggingSetting setting)
+ {
+ if (setting == eStreamDebuggingSetting.Off)
+ {
+ DisableDebugging();
+ return;
+ }
+
+ SetDebuggingWithSpecificTimeout(setting, _DefaultDebugTimeoutMin);
+ }
+
+ ///
+ /// Sets the debugging setting for the specified number of minutes
+ ///
+ ///
+ ///
+ 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);
+
+ }
+
+ ///
+ /// Disabled debugging
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// The available settings for stream debugging
+ ///
+ [Flags]
+ public enum eStreamDebuggingSetting
+ {
+ ///
+ /// Debug off
+ ///
+ Off = 0,
+ ///
+ /// Debug received data
+ ///
+ Rx = 1,
+ ///
+ /// Debug transmitted data
+ ///
+ Tx = 2,
+ ///
+ /// Debug both received and transmitted data
+ ///
+ Both = Rx | Tx
+ }
+
+ ///
+ /// The available settings for stream debugging response types
+ ///
+ [Flags]
+ public enum eStreamDebuggingDataTypeSettings
+ {
+ ///
+ /// Debug data in byte format
+ ///
+ Bytes = 0,
+ ///
+ /// Debug data in text format
+ ///
+ Text = 1,
+ ///
+ /// Debug data in both byte and text formats
+ ///
+ Both = Bytes | Text,
+ }
+}
diff --git a/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs b/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs
new file mode 100644
index 00000000..ff869f77
--- /dev/null
+++ b/src/PepperDash.Core/Comm/ControlPropertiesConfig.cs
@@ -0,0 +1,93 @@
+using System;
+using Crestron.SimplSharp;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Config properties that indicate how to communicate with a device for control
+ ///
+ public class ControlPropertiesConfig
+ {
+ ///
+ /// The method of control
+ ///
+ [JsonProperty("method")]
+ [JsonConverter(typeof(StringEnumConverter))]
+ public eControlMethod Method { get; set; }
+
+ ///
+ /// The key of the device that contains the control port
+ ///
+ [JsonProperty("controlPortDevKey", NullValueHandling = NullValueHandling.Ignore)]
+ public string ControlPortDevKey { get; set; }
+
+ ///
+ /// The number of the control port on the device specified by ControlPortDevKey
+ ///
+ [JsonProperty("controlPortNumber", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value
+ public uint? ControlPortNumber { get; set; }
+
+ ///
+ /// The name of the control port on the device specified by ControlPortDevKey
+ ///
+ [JsonProperty("controlPortName", NullValueHandling = NullValueHandling.Ignore)] // In case "null" is present in config on this value
+ public string ControlPortName { get; set; }
+
+ ///
+ /// Properties for ethernet based communications
+ ///
+ [JsonProperty("tcpSshProperties", NullValueHandling = NullValueHandling.Ignore)]
+ public TcpSshPropertiesConfig TcpSshProperties { get; set; }
+
+ ///
+ /// The filename and path for the IR file
+ ///
+ [JsonProperty("irFile", NullValueHandling = NullValueHandling.Ignore)]
+ public string IrFile { get; set; }
+
+ ///
+ /// The IpId of a Crestron device
+ ///
+ [JsonProperty("ipId", NullValueHandling = NullValueHandling.Ignore)]
+ public string IpId { get; set; }
+
+ ///
+ /// Readonly uint representation of the IpId
+ ///
+ [JsonIgnore]
+ public uint IpIdInt { get { return Convert.ToUInt32(IpId, 16); } }
+
+ ///
+ /// Char indicating end of line
+ ///
+ [JsonProperty("endOfLineChar", NullValueHandling = NullValueHandling.Ignore)]
+ public char EndOfLineChar { get; set; }
+
+ ///
+ /// Defaults to Environment.NewLine;
+ ///
+ [JsonProperty("endOfLineString", NullValueHandling = NullValueHandling.Ignore)]
+ public string EndOfLineString { get; set; }
+
+ ///
+ /// Indicates
+ ///
+ [JsonProperty("deviceReadyResponsePattern", NullValueHandling = NullValueHandling.Ignore)]
+ public string DeviceReadyResponsePattern { get; set; }
+
+ ///
+ /// Used when communcating to programs running in VC-4
+ ///
+ [JsonProperty("roomId", NullValueHandling = NullValueHandling.Ignore)]
+ public string RoomId { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public ControlPropertiesConfig()
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/EventArgs.cs b/src/PepperDash.Core/Comm/EventArgs.cs
new file mode 100644
index 00000000..cf76d6b3
--- /dev/null
+++ b/src/PepperDash.Core/Comm/EventArgs.cs
@@ -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
+{
+ ///
+ /// Delegate for notifying of socket status changes
+ ///
+ ///
+ public delegate void GenericSocketStatusChangeEventDelegate(ISocketStatus client);
+
+ ///
+ /// EventArgs class for socket status changes
+ ///
+ public class GenericSocketStatusChageEventArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public ISocketStatus Client { get; private set; }
+
+ ///
+ ///
+ ///
+ ///
+ public GenericSocketStatusChageEventArgs(ISocketStatus client)
+ {
+ Client = client;
+ }
+ ///
+ /// S+ Constructor
+ ///
+ public GenericSocketStatusChageEventArgs() { }
+ }
+
+ ///
+ /// Delegate for notifying of TCP Server state changes
+ ///
+ ///
+ public delegate void GenericTcpServerStateChangedEventDelegate(ServerState state);
+
+ ///
+ /// EventArgs class for TCP Server state changes
+ ///
+ public class GenericTcpServerStateChangedEventArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public ServerState State { get; private set; }
+
+ ///
+ ///
+ ///
+ ///
+ public GenericTcpServerStateChangedEventArgs(ServerState state)
+ {
+ State = state;
+ }
+ ///
+ /// S+ Constructor
+ ///
+ public GenericTcpServerStateChangedEventArgs() { }
+ }
+
+ ///
+ /// Delegate for TCP Server socket status changes
+ ///
+ ///
+ ///
+ ///
+ public delegate void GenericTcpServerSocketStatusChangeEventDelegate(object socket, uint clientIndex, SocketStatus clientStatus);
+ ///
+ /// EventArgs for TCP server socket status changes
+ ///
+ public class GenericTcpServerSocketStatusChangeEventArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public object Socket { get; private set; }
+ ///
+ ///
+ ///
+ public uint ReceivedFromClientIndex { get; private set; }
+ ///
+ ///
+ ///
+ public SocketStatus ClientStatus { get; set; }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public GenericTcpServerSocketStatusChangeEventArgs(object socket, SocketStatus clientStatus)
+ {
+ Socket = socket;
+ ClientStatus = clientStatus;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public GenericTcpServerSocketStatusChangeEventArgs(object socket, uint clientIndex, SocketStatus clientStatus)
+ {
+ Socket = socket;
+ ReceivedFromClientIndex = clientIndex;
+ ClientStatus = clientStatus;
+ }
+ ///
+ /// S+ Constructor
+ ///
+ public GenericTcpServerSocketStatusChangeEventArgs() { }
+ }
+
+ ///
+ /// EventArgs for TCP server com method receive text
+ ///
+ public class GenericTcpServerCommMethodReceiveTextArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public uint ReceivedFromClientIndex { get; private set; }
+
+ ///
+ ///
+ ///
+ public ushort ReceivedFromClientIndexShort
+ {
+ get
+ {
+ return (ushort)ReceivedFromClientIndex;
+ }
+ }
+
+ ///
+ ///
+ ///
+ public string Text { get; private set; }
+
+ ///
+ ///
+ ///
+ ///
+ public GenericTcpServerCommMethodReceiveTextArgs(string text)
+ {
+ Text = text;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public GenericTcpServerCommMethodReceiveTextArgs(string text, uint clientIndex)
+ {
+ Text = text;
+ ReceivedFromClientIndex = clientIndex;
+ }
+ ///
+ /// S+ Constructor
+ ///
+ public GenericTcpServerCommMethodReceiveTextArgs() { }
+ }
+
+ ///
+ /// EventArgs for TCP server client ready for communication
+ ///
+ public class GenericTcpServerClientReadyForcommunicationsEventArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public bool IsReady;
+
+ ///
+ ///
+ ///
+ ///
+ public GenericTcpServerClientReadyForcommunicationsEventArgs(bool isReady)
+ {
+ IsReady = isReady;
+ }
+ ///
+ /// S+ Constructor
+ ///
+ public GenericTcpServerClientReadyForcommunicationsEventArgs() { }
+ }
+
+ ///
+ /// EventArgs for UDP connected
+ ///
+ public class GenericUdpConnectedEventArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public ushort UConnected;
+ ///
+ ///
+ ///
+ public bool Connected;
+
+ ///
+ /// Constructor
+ ///
+ public GenericUdpConnectedEventArgs() { }
+
+ ///
+ ///
+ ///
+ ///
+ public GenericUdpConnectedEventArgs(ushort uconnected)
+ {
+ UConnected = uconnected;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public GenericUdpConnectedEventArgs(bool connected)
+ {
+ Connected = connected;
+ }
+
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs
new file mode 100644
index 00000000..5ad2e29d
--- /dev/null
+++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs
@@ -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
+{
+ ///
+ /// A class to handle secure TCP/IP communications with a server
+ ///
+ public class GenericSecureTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
+ {
+ private const string SplusKey = "Uninitialized Secure Tcp _client";
+ ///
+ /// Stream debugging
+ ///
+ public CommunicationStreamDebugging StreamDebugging { get; private set; }
+
+ ///
+ /// Fires when data is received from the server and returns it as a Byte array
+ ///
+ public event EventHandler BytesReceived;
+
+ ///
+ /// Fires when data is received from the server and returns it as text
+ ///
+ public event EventHandler TextReceived;
+
+ #region GenericSecureTcpIpClient Events & Delegates
+
+ ///
+ ///
+ ///
+ //public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
+ public event EventHandler ConnectionChange;
+
+ ///
+ /// Auto reconnect evant handler
+ ///
+ public event EventHandler AutoReconnectTriggered;
+
+ ///
+ /// 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.
+ ///
+ public event EventHandler TextReceivedQueueInvoke;
+
+ ///
+ /// 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.
+ ///
+ public event EventHandler ClientReadyForCommunications;
+
+ #endregion
+
+
+ #region GenricTcpIpClient properties
+
+ private string _hostname;
+
+ ///
+ /// Address of server
+ ///
+ public string Hostname
+ {
+ get { return _hostname; }
+ set
+ {
+ _hostname = value;
+ if (_client != null)
+ {
+ _client.AddressClientConnectedTo = _hostname;
+ }
+ }
+ }
+
+ ///
+ /// Port on server
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// S+ helper
+ ///
+ public ushort UPort
+ {
+ get { return Convert.ToUInt16(Port); }
+ set { Port = Convert.ToInt32(value); }
+ }
+
+ ///
+ /// Defaults to 2000
+ ///
+ public int BufferSize { get; set; }
+
+ ///
+ /// Internal secure client
+ ///
+ private SecureTCPClient _client;
+
+ ///
+ /// Bool showing if socket is connected
+ ///
+ public bool IsConnected
+ {
+ get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsConnected
+ {
+ get { return (ushort)(IsConnected ? 1 : 0); }
+ }
+
+ ///
+ /// _client socket status Read only
+ ///
+ public SocketStatus ClientStatus
+ {
+ get
+ {
+ return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
+ }
+ }
+
+ ///
+ /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
+ /// and IsConnected would be true when this == 2.
+ ///
+ public ushort UStatus
+ {
+ get { return (ushort)ClientStatus; }
+ }
+
+ ///
+ /// Status text shows the message associated with socket status
+ ///
+ public string ClientStatusText { get { return ClientStatus.ToString(); } }
+
+ ///
+ /// Connection failure reason
+ ///
+ public string ConnectionFailure { get { return ClientStatus.ToString(); } }
+
+ ///
+ /// bool to track if auto reconnect should be set on the socket
+ ///
+ public bool AutoReconnect { get; set; }
+
+ ///
+ /// S+ helper for AutoReconnect
+ ///
+ public ushort UAutoReconnect
+ {
+ get { return (ushort)(AutoReconnect ? 1 : 0); }
+ set { AutoReconnect = value == 1; }
+ }
+
+ ///
+ /// Milliseconds to wait before attempting to reconnect. Defaults to 5000
+ ///
+ public int AutoReconnectIntervalMs { get; set; }
+
+ ///
+ /// Flag Set only when the disconnect method is called.
+ ///
+ bool DisconnectCalledByUser;
+
+ ///
+ ///
+ ///
+ public bool Connected
+ {
+ get { return _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
+ }
+
+ // private Timer for auto reconnect
+ private CTimer RetryTimer;
+
+ #endregion
+
+ #region GenericSecureTcpIpClient properties
+
+ ///
+ /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
+ ///
+ public bool SharedKeyRequired { get; set; }
+
+ ///
+ /// S+ helper for requires shared key bool
+ ///
+ public ushort USharedKeyRequired
+ {
+ set
+ {
+ if (value == 1)
+ SharedKeyRequired = true;
+ else
+ SharedKeyRequired = false;
+ }
+ }
+
+ ///
+ /// 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
+ ///
+ public string SharedKey { get; set; }
+
+ ///
+ /// flag to show the client is waiting for the server to send the shared key
+ ///
+ private bool WaitingForSharedKeyResponse { get; set; }
+
+ ///
+ /// Semaphore on connect method
+ ///
+ bool IsTryingToConnect;
+
+ ///
+ /// Bool showing if socket is ready for communication after shared key exchange
+ ///
+ public bool IsReadyForCommunication { get; set; }
+
+ ///
+ /// S+ helper for IsReadyForCommunication
+ ///
+ public ushort UIsReadyForCommunication
+ {
+ get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
+ }
+
+ ///
+ /// Bool Heartbeat Enabled flag
+ ///
+ public bool HeartbeatEnabled { get; set; }
+
+ ///
+ /// S+ helper for Heartbeat Enabled
+ ///
+ public ushort UHeartbeatEnabled
+ {
+ get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
+ set { HeartbeatEnabled = value == 1; }
+ }
+
+ ///
+ /// Heartbeat String
+ ///
+ public string HeartbeatString { get; set; }
+ //public int HeartbeatInterval = 50000;
+
+ ///
+ /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
+ ///
+ public int HeartbeatInterval { get; set; }
+
+ ///
+ /// Simpl+ Heartbeat Analog value in seconds
+ ///
+ 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;
+
+ ///
+ /// Queue lock
+ ///
+ CCriticalSection DequeueLock = new CCriticalSection();
+
+ ///
+ /// 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.
+ ///
+ public int ReceiveQueueSize { get; set; }
+
+ ///
+ /// 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.
+ ///
+ private CrestronQueue MessageQueue;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Contstructor that sets all properties by calling the initialize method with a config object.
+ ///
+ ///
+ ///
+ public GenericSecureTcpIpClient(string key, TcpClientConfigObject clientConfigObject)
+ : base(key)
+ {
+ StreamDebugging = new CommunicationStreamDebugging(key);
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ AutoReconnectIntervalMs = 5000;
+ BufferSize = 2000;
+
+ Initialize(clientConfigObject);
+ }
+
+ ///
+ /// Default constructor for S+
+ ///
+ public GenericSecureTcpIpClient()
+ : base(SplusKey)
+ {
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ AutoReconnectIntervalMs = 5000;
+ BufferSize = 2000;
+ }
+
+ ///
+ /// Just to help S+ set the key
+ ///
+ public void Initialize(string key)
+ {
+ Key = key;
+ }
+
+ ///
+ /// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
+ ///
+ ///
+ 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(ReceiveQueueSize);
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(0, this, "Exception initializing client with key: {0}\rException: {1}", Key, ex);
+ }
+ }
+
+ #endregion
+
+ ///
+ /// Handles closing this up when the program shuts down
+ ///
+ 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();
+ }
+
+ }
+
+ ///
+ /// Deactivate the client
+ ///
+ ///
+ public override bool Deactivate()
+ {
+ if (_client != null)
+ {
+ _client.SocketStatusChange -= this.Client_SocketStatusChange;
+ DisconnectClient();
+ }
+ return true;
+ }
+
+ ///
+ /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
+ ///
+ 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();
+ }
+ }
+
+ ///
+ ///
+ ///
+ 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");
+ }
+ }
+
+ ///
+ /// Does the actual disconnect business
+ ///
+ 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
+
+ ///
+ /// Called from Connect failure or Socket Status change if
+ /// auto reconnect and socket disconnected (Not disconnected by user)
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Receive callback
+ ///
+ ///
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ 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)
+ {
+ this.LogVerbose("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);
+ }
+ }
+
+ ///
+ ///
+ ///
+ void StopWaitForSharedKeyTimer()
+ {
+ if (WaitForSharedKey != null)
+ {
+ WaitForSharedKey.Stop();
+ WaitForSharedKey = null;
+ }
+ }
+
+ ///
+ /// General send method
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// SocketStatusChange Callback
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Helper for ConnectionChange event
+ ///
+ void OnConnectionChange()
+ {
+ var handler = ConnectionChange;
+ if (handler == null) return;
+
+ handler(this, new GenericSocketStatusChageEventArgs(this));
+ }
+
+ ///
+ /// Helper to fire ClientReadyForCommunications event
+ ///
+ void OnClientReadyForcommunications(bool isReady)
+ {
+ IsReadyForCommunication = isReady;
+ if (IsReadyForCommunication)
+ HeartbeatStart();
+
+ var handler = ClientReadyForCommunications;
+ if (handler == null) return;
+
+ handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
+ }
+ #endregion
+ }
+
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs
new file mode 100644
index 00000000..93b195ca
--- /dev/null
+++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs
@@ -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
+{
+ ///
+ /// Generic secure TCP/IP client for server
+ ///
+ public class GenericSecureTcpIpClient_ForServer : Device, IAutoReconnect
+ {
+ ///
+ /// Band aid delegate for choked server
+ ///
+ internal delegate void ConnectionHasHungCallbackDelegate();
+
+ #region Events
+
+ //public event EventHandler BytesReceived;
+
+ ///
+ /// Notifies of text received
+ ///
+ public event EventHandler TextReceived;
+
+ ///
+ /// Notifies of auto reconnect sequence triggered
+ ///
+ public event EventHandler AutoReconnectTriggered;
+
+ ///
+ /// 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.
+ ///
+ public event EventHandler TextReceivedQueueInvoke;
+
+ ///
+ /// Notifies of socket status change
+ ///
+ public event EventHandler ConnectionChange;
+
+
+ ///
+ /// 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.
+ ///
+ internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
+
+ ///
+ /// 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.
+ ///
+ public event EventHandler ClientReadyForCommunications;
+
+ #endregion
+
+ #region Properties & Variables
+
+ ///
+ /// Address of server
+ ///
+ public string Hostname { get; set; }
+
+ ///
+ /// Port on server
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// S+ helper
+ ///
+ public ushort UPort
+ {
+ get { return Convert.ToUInt16(Port); }
+ set { Port = Convert.ToInt32(value); }
+ }
+
+ ///
+ /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
+ ///
+ public bool SharedKeyRequired { get; set; }
+
+ ///
+ /// S+ helper for requires shared key bool
+ ///
+ public ushort USharedKeyRequired
+ {
+ set
+ {
+ if (value == 1)
+ SharedKeyRequired = true;
+ else
+ SharedKeyRequired = false;
+ }
+ }
+
+ ///
+ /// 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
+ ///
+ public string SharedKey { get; set; }
+
+ ///
+ /// flag to show the client is waiting for the server to send the shared key
+ ///
+ private bool WaitingForSharedKeyResponse { get; set; }
+
+ ///
+ /// Defaults to 2000
+ ///
+ public int BufferSize { get; set; }
+
+ ///
+ /// Semaphore on connect method
+ ///
+ bool IsTryingToConnect;
+
+ ///
+ /// Bool showing if socket is connected
+ ///
+ public bool IsConnected
+ {
+ get
+ {
+ if (Client != null)
+ return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
+ else
+ return false;
+ }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsConnected
+ {
+ get { return (ushort)(IsConnected ? 1 : 0); }
+ }
+
+ ///
+ /// Bool showing if socket is ready for communication after shared key exchange
+ ///
+ public bool IsReadyForCommunication { get; set; }
+
+ ///
+ /// S+ helper for IsReadyForCommunication
+ ///
+ public ushort UIsReadyForCommunication
+ {
+ get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
+ }
+
+ ///
+ /// Client socket status Read only
+ ///
+ public SocketStatus ClientStatus
+ {
+ get
+ {
+ if (Client != null)
+ return Client.ClientStatus;
+ else
+ return SocketStatus.SOCKET_STATUS_NO_CONNECT;
+ }
+ }
+
+ ///
+ /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
+ /// and IsConnected would be true when this == 2.
+ ///
+ public ushort UStatus
+ {
+ get { return (ushort)ClientStatus; }
+ }
+
+ ///
+ /// Status text shows the message associated with socket status
+ ///
+ public string ClientStatusText { get { return ClientStatus.ToString(); } }
+
+ ///
+ /// bool to track if auto reconnect should be set on the socket
+ ///
+ public bool AutoReconnect { get; set; }
+
+ ///
+ /// S+ helper for AutoReconnect
+ ///
+ public ushort UAutoReconnect
+ {
+ get { return (ushort)(AutoReconnect ? 1 : 0); }
+ set { AutoReconnect = value == 1; }
+ }
+ ///
+ /// Milliseconds to wait before attempting to reconnect. Defaults to 5000
+ ///
+ public int AutoReconnectIntervalMs { get; set; }
+
+ ///
+ /// Flag Set only when the disconnect method is called.
+ ///
+ bool DisconnectCalledByUser;
+
+ ///
+ /// private Timer for auto reconnect
+ ///
+ CTimer RetryTimer;
+
+
+ ///
+ ///
+ ///
+ public bool HeartbeatEnabled { get; set; }
+ ///
+ ///
+ ///
+ public ushort UHeartbeatEnabled
+ {
+ get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
+ set { HeartbeatEnabled = value == 1; }
+ }
+
+ ///
+ ///
+ ///
+ public string HeartbeatString { get; set; }
+ //public int HeartbeatInterval = 50000;
+
+ ///
+ /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
+ ///
+ public int HeartbeatInterval { get; set; }
+
+ ///
+ /// Simpl+ Heartbeat Analog value in seconds
+ ///
+ 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;
+ ///
+ /// Internal secure client
+ ///
+ SecureTCPClient Client;
+
+ bool ProgramIsStopping;
+
+ ///
+ /// Queue lock
+ ///
+ CCriticalSection DequeueLock = new CCriticalSection();
+
+ ///
+ /// 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.
+ ///
+ public int ReceiveQueueSize { get; set; }
+
+ ///
+ /// 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.
+ ///
+ private CrestronQueue MessageQueue;
+
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+
+ }
+
+ ///
+ /// Constructor for S+
+ ///
+ public GenericSecureTcpIpClient_ForServer()
+ : base("Uninitialized Secure Tcp Client For Server")
+ {
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ AutoReconnectIntervalMs = 5000;
+ BufferSize = 2000;
+ }
+
+ ///
+ /// Contstructor that sets all properties by calling the initialize method with a config object.
+ ///
+ ///
+ ///
+ public GenericSecureTcpIpClient_ForServer(string key, TcpClientConfigObject clientConfigObject)
+ : base(key)
+ {
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ Initialize(clientConfigObject);
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Just to help S+ set the key
+ ///
+ public void Initialize(string key)
+ {
+ Key = key;
+ }
+
+ ///
+ /// Initialize called by the constructor that accepts a client config object. Can be called later to reset properties of client.
+ ///
+ ///
+ 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(ReceiveQueueSize);
+ }
+ else
+ {
+ ErrorLog.Error("Could not initialize client with key: {0}", Key);
+ }
+ }
+ catch
+ {
+ ErrorLog.Error("Could not initialize client with key: {0}", Key);
+ }
+ }
+
+ ///
+ /// Handles closing this up when the program shuts down
+ ///
+ 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();
+ }
+
+ }
+
+ ///
+ /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
+ ///
+ 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();
+ }
+ }
+
+ ///
+ ///
+ ///
+ public void Disconnect()
+ {
+ this.LogVerbose("Disconnect Called");
+
+ DisconnectCalledByUser = true;
+ if (IsConnected)
+ {
+ Client.DisconnectFromServer();
+
+ }
+ if (RetryTimer != null)
+ {
+ RetryTimer.Stop();
+ RetryTimer = null;
+ }
+ Cleanup();
+ }
+
+ ///
+ /// Internal call to close up client. ALWAYS use this when disconnecting.
+ ///
+ 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;
+ }
+ }
+
+
+ /// ff
+ /// Called from Connect failure or Socket Status change if
+ /// auto reconnect and socket disconnected (Not disconnected by user)
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Receive callback
+ ///
+ ///
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ 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);
+ }
+ }
+
+ ///
+ ///
+ ///
+ void StopWaitForSharedKeyTimer()
+ {
+ if (WaitForSharedKey != null)
+ {
+ WaitForSharedKey.Stop();
+ WaitForSharedKey = null;
+ }
+ }
+
+ ///
+ /// General send method
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// SocketStatusChange Callback
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Helper for ConnectionChange event
+ ///
+ void OnConnectionChange()
+ {
+ var handler = ConnectionChange;
+ if (handler != null)
+ ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
+ }
+
+ ///
+ /// Helper to fire ClientReadyForCommunications event
+ ///
+ void OnClientReadyForcommunications(bool isReady)
+ {
+ IsReadyForCommunication = isReady;
+ if (this.IsReadyForCommunication) { HeartbeatStart(); }
+ var handler = ClientReadyForCommunications;
+ if (handler != null)
+ handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
+ }
+ #endregion
+ }
+
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs
new file mode 100644
index 00000000..e0da068f
--- /dev/null
+++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs
@@ -0,0 +1,1084 @@
+/*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 Crestron.SimplSharp;
+using Crestron.SimplSharp.CrestronSockets;
+using PepperDash.Core.Logging;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Generic secure TCP/IP server
+ ///
+ public class GenericSecureTcpIpServer : Device
+ {
+ #region Events
+ ///
+ /// Event for Receiving text
+ ///
+ public event EventHandler TextReceived;
+
+ ///
+ /// 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.
+ ///
+ public event EventHandler TextReceivedQueueInvoke;
+
+ ///
+ /// Event for client connection socket status change
+ ///
+ public event EventHandler ClientConnectionChange;
+
+ ///
+ /// Event for Server State Change
+ ///
+ public event EventHandler ServerStateChange;
+
+ ///
+ /// For a server with a pre shared key, this will fire after the communication is established and the key exchange is complete. If no shared key, this will fire
+ /// after connection is successful. Use this event to know when the client is ready for communication to avoid stepping on shared key.
+ ///
+ public event EventHandler ServerClientReadyForCommunications;
+
+ ///
+ /// A band aid event to notify user that the server has choked.
+ ///
+ public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; }
+
+ ///
+ ///
+ ///
+ public delegate void ServerHasChokedCallbackDelegate();
+
+ #endregion
+
+ #region Properties/Variables
+
+ ///
+ /// Server listen lock
+ ///
+ CCriticalSection ServerCCSection = new CCriticalSection();
+
+ ///
+ /// Queue lock
+ ///
+ CCriticalSection DequeueLock = new CCriticalSection();
+
+ ///
+ /// 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.
+ ///
+ public int ReceiveQueueSize { get; set; }
+
+ ///
+ /// 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.
+ ///
+ private CrestronQueue MessageQueue;
+
+ ///
+ /// A bandaid client that monitors whether the server is reachable
+ ///
+ GenericSecureTcpIpClient_ForServer MonitorClient;
+
+ ///
+ /// Timer to operate the bandaid monitor client in a loop.
+ ///
+ CTimer MonitorClientTimer;
+
+ ///
+ ///
+ ///
+ int MonitorClientFailureCount;
+
+ ///
+ /// 3 by default
+ ///
+ public int MonitorClientMaxFailureCount { get; set; }
+
+ ///
+ /// Text representation of the Socket Status enum values for the server
+ ///
+ public string Status
+ {
+ get
+ {
+ if (SecureServer != null)
+ return SecureServer.State.ToString();
+ return ServerState.SERVER_NOT_LISTENING.ToString();
+
+ }
+
+ }
+
+ ///
+ /// Bool showing if socket is connected
+ ///
+ public bool IsConnected
+ {
+ get
+ {
+ if (SecureServer != null)
+ return (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED;
+ return false;
+
+ //return (Secure ? SecureServer != null : UnsecureServer != null) &&
+ //(Secure ? (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED :
+ // (UnsecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED);
+ }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsConnected
+ {
+ get { return (ushort)(IsConnected ? 1 : 0); }
+ }
+
+ ///
+ /// Bool showing if socket is connected
+ ///
+ public bool IsListening
+ {
+ get
+ {
+ if (SecureServer != null)
+ return (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING;
+ else
+ return false;
+ //return (Secure ? SecureServer != null : UnsecureServer != null) &&
+ //(Secure ? (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING :
+ // (UnsecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING);
+ }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsListening
+ {
+ get { return (ushort)(IsListening ? 1 : 0); }
+ }
+ ///
+ /// Max number of clients this server will allow for connection. Crestron max is 64. This number should be less than 65
+ ///
+ public ushort MaxClients { get; set; } // should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable
+ ///
+ /// Number of clients currently connected.
+ ///
+ public ushort NumberOfClientsConnected
+ {
+ get
+ {
+ if (SecureServer != null)
+ return (ushort)SecureServer.NumberOfClientsConnected;
+ return 0;
+ }
+ }
+
+ ///
+ /// Port Server should listen on
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// S+ helper for Port
+ ///
+ public ushort UPort
+ {
+ get { return Convert.ToUInt16(Port); }
+ set { Port = Convert.ToInt32(value); }
+ }
+
+ ///
+ /// Bool to show whether the server requires a preshared key. Must be set the same in the client, and if true shared keys must be identical on server/client
+ ///
+ public bool SharedKeyRequired { get; set; }
+
+ ///
+ /// S+ helper for requires shared key bool
+ ///
+ public ushort USharedKeyRequired
+ {
+ set
+ {
+ if (value == 1)
+ SharedKeyRequired = true;
+ else
+ SharedKeyRequired = false;
+ }
+ }
+
+ ///
+ /// 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.
+ /// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called
+ ///
+ public string SharedKey { get; set; }
+
+ ///
+ /// Heartbeat Required bool sets whether server disconnects client if heartbeat is not received
+ ///
+ public bool HeartbeatRequired { get; set; }
+
+ ///
+ /// S+ Helper for Heartbeat Required
+ ///
+ public ushort UHeartbeatRequired
+ {
+ set
+ {
+ if (value == 1)
+ HeartbeatRequired = true;
+ else
+ HeartbeatRequired = false;
+ }
+ }
+
+ ///
+ /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
+ ///
+ public int HeartbeatRequiredIntervalMs { get; set; }
+
+ ///
+ /// Simpl+ Heartbeat Analog value in seconds
+ ///
+ public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } }
+
+ ///
+ /// String to Match for heartbeat. If null or empty any string will reset heartbeat timer
+ ///
+ public string HeartbeatStringToMatch { get; set; }
+
+ //private timers for Heartbeats per client
+ Dictionary HeartbeatTimerDictionary = new Dictionary();
+
+ //flags to show the secure server is waiting for client at index to send the shared key
+ List WaitingForSharedKey = new List();
+
+ List ClientReadyAfterKeyExchange = new List();
+
+ ///
+ /// The connected client indexes
+ ///
+ public List ConnectedClientsIndexes = new List();
+
+ ///
+ /// Defaults to 2000
+ ///
+ public int BufferSize { get; set; }
+
+ ///
+ /// Private flag to note that the server has stopped intentionally
+ ///
+ private bool ServerStopped { get; set; }
+
+ //Servers
+ SecureTCPServer SecureServer;
+
+ ///
+ ///
+ ///
+ bool ProgramIsStopping;
+
+ #endregion
+
+ #region Constructors
+ ///
+ /// constructor S+ Does not accept a key. Use initialze with key to set the debug key on this device. If using with + make sure to set all properties manually.
+ ///
+ public GenericSecureTcpIpServer()
+ : base("Uninitialized Secure TCP Server")
+ {
+ HeartbeatRequiredIntervalInSeconds = 15;
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ BufferSize = 2000;
+ MonitorClientMaxFailureCount = 3;
+ }
+
+ ///
+ /// constructor with debug key set at instantiation. Make sure to set all properties before listening.
+ ///
+ ///
+ public GenericSecureTcpIpServer(string key)
+ : base("Uninitialized Secure TCP Server")
+ {
+ HeartbeatRequiredIntervalInSeconds = 15;
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ BufferSize = 2000;
+ MonitorClientMaxFailureCount = 3;
+ Key = key;
+ }
+
+ ///
+ /// Contstructor that sets all properties by calling the initialize method with a config object. This does set Queue size.
+ ///
+ ///
+ public GenericSecureTcpIpServer(TcpServerConfigObject serverConfigObject)
+ : base("Uninitialized Secure TCP Server")
+ {
+ HeartbeatRequiredIntervalInSeconds = 15;
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ BufferSize = 2000;
+ MonitorClientMaxFailureCount = 3;
+ Initialize(serverConfigObject);
+ }
+ #endregion
+
+ #region Methods - Server Actions
+ ///
+ /// Disconnects all clients and stops the server
+ ///
+ public void KillServer()
+ {
+ ServerStopped = true;
+ if (MonitorClient != null)
+ {
+ MonitorClient.Disconnect();
+ }
+ DisconnectAllClientsForShutdown();
+ StopListening();
+ }
+
+ ///
+ /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
+ ///
+ ///
+ public void Initialize(string key)
+ {
+ Key = key;
+ }
+
+ ///
+ /// Initialze the server
+ ///
+ ///
+ public void Initialize(TcpServerConfigObject serverConfigObject)
+ {
+ try
+ {
+ if (serverConfigObject != null || string.IsNullOrEmpty(serverConfigObject.Key))
+ {
+ Key = serverConfigObject.Key;
+ MaxClients = serverConfigObject.MaxClients;
+ Port = serverConfigObject.Port;
+ SharedKeyRequired = serverConfigObject.SharedKeyRequired;
+ SharedKey = serverConfigObject.SharedKey;
+ HeartbeatRequired = serverConfigObject.HeartbeatRequired;
+ HeartbeatRequiredIntervalInSeconds = serverConfigObject.HeartbeatRequiredIntervalInSeconds;
+ HeartbeatStringToMatch = serverConfigObject.HeartbeatStringToMatch;
+ BufferSize = serverConfigObject.BufferSize;
+ ReceiveQueueSize = serverConfigObject.ReceiveQueueSize > 20 ? serverConfigObject.ReceiveQueueSize : 20;
+ MessageQueue = new CrestronQueue(ReceiveQueueSize);
+ }
+ else
+ {
+ ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key);
+ }
+ }
+ catch
+ {
+ ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key);
+ }
+ }
+
+ ///
+ /// Start listening on the specified port
+ ///
+ public void Listen()
+ {
+ ServerCCSection.Enter();
+ try
+ {
+ if (Port < 1 || Port > 65535)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key);
+ ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
+ return;
+ }
+ if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key);
+ ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
+ return;
+ }
+
+
+ if (SecureServer == null)
+ {
+ SecureServer = new SecureTCPServer(Port, MaxClients);
+ if (HeartbeatRequired)
+ SecureServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5);
+ SecureServer.HandshakeTimeout = 30;
+ SecureServer.SocketStatusChange += new SecureTCPServerSocketStatusChangeEventHandler(SecureServer_SocketStatusChange);
+ }
+ else
+ {
+ SecureServer.PortNumber = Port;
+ }
+ ServerStopped = false;
+
+ // Start the listner
+ SocketErrorCodes status = SecureServer.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
+ if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
+ {
+ Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Error starting WaitForConnectionAsync {0}", status);
+ }
+ else
+ {
+ ServerStopped = false;
+ }
+ OnServerStateChange(SecureServer.State);
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Secure Server Status: {0}, Socket Status: {1}", SecureServer.State, SecureServer.ServerSocketStatus);
+ ServerCCSection.Leave();
+
+ }
+ catch (Exception ex)
+ {
+ ServerCCSection.Leave();
+ ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
+ }
+ }
+
+ ///
+ /// Stop Listeneing
+ ///
+ public void StopListening()
+ {
+ try
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener");
+ if (SecureServer != null)
+ {
+ SecureServer.Stop();
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", SecureServer.State);
+ OnServerStateChange(SecureServer.State);
+ }
+ ServerStopped = true;
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex);
+ }
+ }
+
+ ///
+ /// Disconnects Client
+ ///
+ ///
+ public void DisconnectClient(uint client)
+ {
+ try
+ {
+ SecureServer.Disconnect(client);
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client);
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex);
+ }
+ }
+ ///
+ /// Disconnect All Clients
+ ///
+ public void DisconnectAllClientsForShutdown()
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
+ if (SecureServer != null)
+ {
+ SecureServer.SocketStatusChange -= SecureServer_SocketStatusChange;
+ foreach (var index in ConnectedClientsIndexes.ToList()) // copy it here so that it iterates properly
+ {
+ var i = index;
+ if (!SecureServer.ClientConnected(index))
+ continue;
+ try
+ {
+ SecureServer.Disconnect(i);
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i);
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex);
+ }
+ }
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", SecureServer.ServerSocketStatus);
+ }
+
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients");
+ ConnectedClientsIndexes.Clear();
+
+ if (!ProgramIsStopping)
+ {
+ OnConnectionChange();
+ OnServerStateChange(SecureServer.State); //State shows both listening and connected
+ }
+
+ // var o = new { };
+ }
+
+ ///
+ /// Broadcast text from server to all connected clients
+ ///
+ ///
+ public void BroadcastText(string text)
+ {
+ CCriticalSection CCBroadcast = new CCriticalSection();
+ CCBroadcast.Enter();
+ try
+ {
+ if (ConnectedClientsIndexes.Count > 0)
+ {
+ byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
+ foreach (uint i in ConnectedClientsIndexes)
+ {
+ if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(i)))
+ {
+ SocketErrorCodes error = SecureServer.SendDataAsync(i, b, b.Length, (x, y, z) => { });
+ if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING)
+ this.LogVerbose("{error}", error);
+ }
+ }
+ }
+ CCBroadcast.Leave();
+ }
+ catch (Exception ex)
+ {
+ CCBroadcast.Leave();
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
+ }
+ }
+
+ ///
+ /// Not sure this is useful in library, maybe Pro??
+ ///
+ ///
+ ///
+ public void SendTextToClient(string text, uint clientIndex)
+ {
+ try
+ {
+ byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
+ if (SecureServer != null && SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
+ {
+ if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
+ SecureServer.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
+ }
+ }
+
+ //private method to check heartbeat requirements and start or reset timer
+ string checkHeartbeat(uint clientIndex, string received)
+ {
+ try
+ {
+ if (HeartbeatRequired)
+ {
+ if (!string.IsNullOrEmpty(HeartbeatStringToMatch))
+ {
+ var remainingText = received.Replace(HeartbeatStringToMatch, "");
+ var noDelimiter = received.Trim(new char[] { '\r', '\n' });
+ if (noDelimiter.Contains(HeartbeatStringToMatch))
+ {
+ if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
+ else
+ {
+ CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
+ HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
+ }
+ Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
+ // Return Heartbeat
+ SendTextToClient(HeartbeatStringToMatch, clientIndex);
+ return remainingText;
+ }
+ }
+ else
+ {
+ if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
+ else
+ {
+ CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
+ HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
+ }
+ Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
+ }
+ return received;
+ }
+
+ ///
+ /// Get the IP Address for the client at the specifed index
+ ///
+ ///
+ ///
+ public string GetClientIPAddress(uint clientIndex)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex);
+ if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
+ {
+ var ipa = this.SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa);
+ return ipa;
+
+ }
+ else
+ {
+ return "";
+ }
+ }
+
+ #endregion
+
+ #region Methods - HeartbeatTimer Callback
+
+ void HeartbeatTimer_CallbackFunction(object o)
+ {
+ uint clientIndex = 99999;
+ string address = string.Empty;
+ try
+ {
+ clientIndex = (uint)o;
+ address = SecureServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
+
+ Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
+ address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
+
+ if (SecureServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
+ SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
+
+ var discoResult = SecureServer.Disconnect(clientIndex);
+ //Debug.Console(1, this, "{0}", discoResult);
+
+ if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ {
+ HeartbeatTimerDictionary[clientIndex].Stop();
+ HeartbeatTimerDictionary[clientIndex].Dispose();
+ HeartbeatTimerDictionary.Remove(clientIndex);
+ }
+ }
+ catch (Exception ex)
+ {
+ ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key);
+ }
+ }
+
+ #endregion
+
+ #region Methods - Socket Status Changed Callbacks
+ ///
+ /// Secure Server Socket Status Changed Callback
+ ///
+ ///
+ ///
+ ///
+ void SecureServer_SocketStatusChange(SecureTCPServer server, uint clientIndex, SocketStatus serverSocketStatus)
+ {
+ try
+ {
+
+
+ // Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.SecureServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.SecureServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
+ if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange ConnectedCLients: {0} ServerState: {1} Port: {2}", SecureServer.NumberOfClientsConnected, SecureServer.State, SecureServer.PortNumber);
+
+ if (ConnectedClientsIndexes.Contains(clientIndex))
+ ConnectedClientsIndexes.Remove(clientIndex);
+ if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ {
+ HeartbeatTimerDictionary[clientIndex].Stop();
+ HeartbeatTimerDictionary[clientIndex].Dispose();
+ HeartbeatTimerDictionary.Remove(clientIndex);
+ }
+ if (ClientReadyAfterKeyExchange.Contains(clientIndex))
+ ClientReadyAfterKeyExchange.Remove(clientIndex);
+ if (WaitingForSharedKey.Contains(clientIndex))
+ WaitingForSharedKey.Remove(clientIndex);
+ if (SecureServer.MaxNumberOfClientSupported > SecureServer.NumberOfClientsConnected)
+ {
+ Listen();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex);
+ }
+ //Use a thread for this event so that the server state updates to listening while this event is processed. Listening must be added to the server state
+ //after every client connection so that the server can check and see if it is at max clients. Due to this the event fires and server listening enum bit flag
+ //is not set. Putting in a thread allows the state to update before this event processes so that the subscribers to this event get accurate isListening in the event.
+ CrestronInvoke.BeginInvoke(o => onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex)), null);
+ }
+
+ #endregion
+
+ #region Methods Connected Callbacks
+ ///
+ /// Secure TCP Client Connected to Secure Server Callback
+ ///
+ ///
+ ///
+ void SecureConnectCallback(SecureTCPServer server, uint clientIndex)
+ {
+ try
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
+ server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
+ clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
+ if (clientIndex != 0)
+ {
+ if (server.ClientConnected(clientIndex))
+ {
+
+ if (!ConnectedClientsIndexes.Contains(clientIndex))
+ {
+ ConnectedClientsIndexes.Add(clientIndex);
+ }
+ if (SharedKeyRequired)
+ {
+ if (!WaitingForSharedKey.Contains(clientIndex))
+ {
+ WaitingForSharedKey.Add(clientIndex);
+ }
+ byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
+ server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
+ }
+ else
+ {
+ OnServerClientReadyForCommunications(clientIndex);
+ }
+ if (HeartbeatRequired)
+ {
+ if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ {
+ HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
+ }
+ }
+
+ server.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
+ }
+ }
+ else
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex);
+ }
+
+ // Rearm the listner
+ SocketErrorCodes status = server.WaitForConnectionAsync(IPAddress.Any, SecureConnectCallback);
+ if (status != SocketErrorCodes.SOCKET_OPERATION_PENDING)
+ {
+ Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Socket status connect callback status {0}", status);
+ if (status == SocketErrorCodes.SOCKET_CONNECTION_IN_PROGRESS)
+ {
+ // There is an issue where on a failed negotiation we need to stop and start the server. This should still leave connected clients intact.
+ server.Stop();
+ Listen();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Methods - Send/Receive Callbacks
+ ///
+ /// Secure Received Data Async Callback
+ ///
+ ///
+ ///
+ ///
+ void SecureReceivedDataAsyncCallback(SecureTCPServer mySecureTCPServer, uint clientIndex, int numberOfBytesReceived)
+ {
+ if (numberOfBytesReceived > 0)
+ {
+
+ string received = "Nothing";
+ var handler = TextReceivedQueueInvoke;
+ try
+ {
+ byte[] bytes = mySecureTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex);
+ received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived);
+ if (WaitingForSharedKey.Contains(clientIndex))
+ {
+ received = received.Replace("\r", "");
+ received = received.Replace("\n", "");
+ if (received != SharedKey)
+ {
+ byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
+ Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
+ mySecureTCPServer.SendData(clientIndex, b, b.Length);
+ mySecureTCPServer.Disconnect(clientIndex);
+
+ return;
+ }
+
+ WaitingForSharedKey.Remove(clientIndex);
+ byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
+ mySecureTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
+ OnServerClientReadyForCommunications(clientIndex);
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
+ }
+ else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
+ {
+ onTextReceived(received, clientIndex);
+ if (handler != null)
+ {
+ MessageQueue.TryToEnqueue(new GenericTcpServerCommMethodReceiveTextArgs(received, clientIndex));
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex);
+ }
+ if (mySecureTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
+ mySecureTCPServer.ReceiveDataAsync(clientIndex, SecureReceivedDataAsyncCallback);
+
+ //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
+ {
+ mySecureTCPServer.Disconnect(clientIndex);
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ 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();
+ }
+ }
+
+ #endregion
+
+ #region Methods - EventHelpers/Callbacks
+
+ //Private Helper method to call the Connection Change Event
+ void onConnectionChange(uint clientIndex, SocketStatus clientStatus)
+ {
+ if (clientIndex != 0) //0 is error not valid client change
+ {
+ var handler = ClientConnectionChange;
+ if (handler != null)
+ {
+ handler(this, new GenericTcpServerSocketStatusChangeEventArgs(SecureServer, clientIndex, clientStatus));
+ }
+ }
+ }
+
+ //Private Helper method to call the Connection Change Event
+ void OnConnectionChange()
+ {
+ if (ProgramIsStopping)
+ {
+ return;
+ }
+ var handler = ClientConnectionChange;
+ if (handler != null)
+ {
+ handler(this, new GenericTcpServerSocketStatusChangeEventArgs());
+ }
+ }
+
+ //Private Helper Method to call the Text Received Event
+ void onTextReceived(string text, uint clientIndex)
+ {
+ var handler = TextReceived;
+ if (handler != null)
+ handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex));
+ }
+
+ //Private Helper Method to call the Server State Change Event
+ void OnServerStateChange(ServerState state)
+ {
+ if (ProgramIsStopping)
+ {
+ return;
+ }
+ var handler = ServerStateChange;
+ if (handler != null)
+ {
+ handler(this, new GenericTcpServerStateChangedEventArgs(state));
+ }
+ }
+
+ ///
+ /// Private Event Handler method to handle the closing of connections when the program stops
+ ///
+ ///
+ void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ {
+ if (programEventType == eProgramStatusEventType.Stopping)
+ {
+ ProgramIsStopping = true;
+ // kill bandaid things
+ if (MonitorClientTimer != null)
+ MonitorClientTimer.Stop();
+ if (MonitorClient != null)
+ MonitorClient.Disconnect();
+
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server");
+ KillServer();
+ }
+ }
+
+ //Private event handler method to raise the event that the server is ready to send data after a successful client shared key negotiation
+ void OnServerClientReadyForCommunications(uint clientIndex)
+ {
+ ClientReadyAfterKeyExchange.Add(clientIndex);
+ var handler = ServerClientReadyForCommunications;
+ if (handler != null)
+ handler(this, new GenericTcpServerSocketStatusChangeEventArgs(
+ this, clientIndex, SecureServer.GetServerSocketStatusForSpecificClient(clientIndex)));
+ }
+ #endregion
+
+ #region Monitor Client
+ ///
+ /// Starts the monitor client cycle. Timed wait, then call RunMonitorClient
+ ///
+ void StartMonitorClient()
+ {
+ if (MonitorClientTimer != null)
+ {
+ return;
+ }
+ MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
+ }
+
+ ///
+ ///
+ ///
+ void RunMonitorClient()
+ {
+ MonitorClient = new GenericSecureTcpIpClient_ForServer(Key + "-MONITOR", "127.0.0.1", Port, 2000);
+ MonitorClient.SharedKeyRequired = this.SharedKeyRequired;
+ MonitorClient.SharedKey = this.SharedKey;
+ MonitorClient.ConnectionHasHungCallback = MonitorClientHasHungCallback;
+ //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
+ MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
+
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check");
+
+ MonitorClient.Connect();
+ // From here MonitorCLient either connects or hangs, MonitorClient will call back
+
+ }
+
+ ///
+ ///
+ ///
+ void StopMonitorClient()
+ {
+ if (MonitorClient == null)
+ return;
+
+ MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm;
+ MonitorClient.Disconnect();
+ MonitorClient = null;
+ }
+
+ ///
+ /// On monitor connect, restart the operation
+ ///
+ void MonitorClient_IsReadyForComm(object sender, GenericTcpServerClientReadyForcommunicationsEventArgs args)
+ {
+ if (args.IsReady)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s");
+ MonitorClientTimer.Stop();
+ MonitorClientTimer = null;
+ MonitorClientFailureCount = 0;
+ CrestronEnvironment.Sleep(2000);
+ StopMonitorClient();
+ StartMonitorClient();
+ }
+ }
+
+ ///
+ /// If the client hangs, add to counter and maybe fire the choke event
+ ///
+ void MonitorClientHasHungCallback()
+ {
+ MonitorClientFailureCount++;
+ MonitorClientTimer.Stop();
+ MonitorClientTimer = null;
+ StopMonitorClient();
+ if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}",
+ MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
+ StartMonitorClient();
+ }
+ else
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error,
+ "\r***************************\rMonitor client connection has hung a maximum of {0} times. \r***************************",
+ MonitorClientMaxFailureCount);
+
+ var handler = ServerHasChoked;
+ if (handler != null)
+ handler();
+ // Some external thing is in charge here. Expected reset of program
+ }
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/GenericSshClient.cs b/src/PepperDash.Core/Comm/GenericSshClient.cs
new file mode 100644
index 00000000..fa5b95bb
--- /dev/null
+++ b/src/PepperDash.Core/Comm/GenericSshClient.cs
@@ -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
+{
+ ///
+ ///
+ ///
+ public class GenericSshClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
+ {
+ private const string SPlusKey = "Uninitialized SshClient";
+ ///
+ /// Object to enable stream debugging
+ ///
+ public CommunicationStreamDebugging StreamDebugging { get; private set; }
+
+ ///
+ /// Event that fires when data is received. Delivers args with byte array
+ ///
+ public event EventHandler BytesReceived;
+
+ ///
+ /// Event that fires when data is received. Delivered as text.
+ ///
+ public event EventHandler TextReceived;
+
+ ///
+ /// Event when the connection status changes.
+ ///
+ public event EventHandler ConnectionChange;
+
+ /////
+ /////
+ /////
+ //public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
+
+ ///
+ /// Address of server
+ ///
+ public string Hostname { get; set; }
+
+ ///
+ /// Port on server
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// Username for server
+ ///
+ public string Username { get; set; }
+
+ ///
+ /// And... Password for server. That was worth documenting!
+ ///
+ public string Password { get; set; }
+
+ ///
+ /// True when the server is connected - when status == 2.
+ ///
+ public bool IsConnected
+ {
+ // returns false if no client or not connected
+ get { return Client != null && ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsConnected
+ {
+ get { return (ushort)(IsConnected ? 1 : 0); }
+ }
+
+ ///
+ ///
+ ///
+ public SocketStatus ClientStatus
+ {
+ get { return _ClientStatus; }
+ private set
+ {
+ if (_ClientStatus == value)
+ return;
+ _ClientStatus = value;
+ OnConnectionChange();
+ }
+ }
+ SocketStatus _ClientStatus;
+
+ ///
+ /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
+ /// and IsConnected with be true when this == 2.
+ ///
+ public ushort UStatus
+ {
+ get { return (ushort)_ClientStatus; }
+ }
+
+ ///
+ /// Determines whether client will attempt reconnection on failure. Default is true
+ ///
+ public bool AutoReconnect { get; set; }
+
+ ///
+ /// Will be set and unset by connect and disconnect only
+ ///
+ public bool ConnectEnabled { get; private set; }
+
+ ///
+ /// S+ helper for AutoReconnect
+ ///
+ public ushort UAutoReconnect
+ {
+ get { return (ushort)(AutoReconnect ? 1 : 0); }
+ set { AutoReconnect = value == 1; }
+ }
+
+ ///
+ /// Millisecond value, determines the timeout period in between reconnect attempts.
+ /// Set to 5000 by default
+ ///
+ 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;
+
+ ///
+ /// Typical constructor.
+ ///
+ 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);
+ }
+
+ ///
+ /// S+ Constructor - Must set all properties before calling Connect
+ ///
+ public GenericSshClient()
+ : base(SPlusKey)
+ {
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ AutoReconnectIntervalMs = 5000;
+
+ ReconnectTimer = new CTimer(o =>
+ {
+ if (ConnectEnabled)
+ {
+ Connect();
+ }
+ }, System.Threading.Timeout.Infinite);
+ }
+
+ ///
+ /// Handles closing this up when the program shuts down
+ ///
+ void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ {
+ if (programEventType == eProgramStatusEventType.Stopping)
+ {
+ if (Client != null)
+ {
+ this.LogDebug("Program stopping. Closing connection");
+ Disconnect();
+ }
+ }
+ }
+
+ ///
+ /// Connect to the server, using the provided properties.
+ ///
+ 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(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();
+ }
+ }
+
+ ///
+ /// Disconnect the clients and put away it's resources.
+ ///
+ public void Disconnect()
+ {
+ ConnectEnabled = false;
+ // Stop trying reconnects, if we are
+ if (ReconnectTimer != null)
+ {
+ ReconnectTimer.Stop();
+ // ReconnectTimer = null;
+ }
+
+ KillClient(SocketStatus.SOCKET_STATUS_BROKEN_LOCALLY);
+ }
+
+ ///
+ /// Kills the stream, cleans up the client and sets it to null
+ ///
+ 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");
+ }
+ }
+
+ ///
+ /// Kills the stream
+ ///
+ 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}");
+ }
+ }
+
+ ///
+ /// Handles the keyboard interactive authentication, should it be required.
+ ///
+ void kauth_AuthenticationPrompt(object sender, AuthenticationPromptEventArgs e)
+ {
+ foreach (AuthenticationPrompt prompt in e.Prompts)
+ if (prompt.Request.IndexOf("Password:", StringComparison.InvariantCultureIgnoreCase) != -1)
+ prompt.Response = Password;
+ }
+
+ ///
+ /// Handler for data receive on ShellStream. Passes data across to queue for line parsing.
+ ///
+ 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));
+ }
+
+ }
+
+
+ ///
+ /// Error event handler for client events - disconnect, etc. Will forward those events via ConnectionChange
+ /// event
+ ///
+ 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);
+ }
+ });
+ }
+
+ ///
+ /// Helper for ConnectionChange event
+ ///
+ void OnConnectionChange()
+ {
+ if (ConnectionChange != null)
+ ConnectionChange(this, new GenericSocketStatusChageEventArgs(this));
+ }
+
+ #region IBasicCommunication Members
+
+ ///
+ /// Sends text to the server
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Sends Bytes to the server
+ ///
+ ///
+ 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
+
+}
+
+//*****************************************************************************************************
+//*****************************************************************************************************
+///
+/// Fired when connection changes
+///
+public class SshConnectionChangeEventArgs : EventArgs
+ {
+ ///
+ /// Connection State
+ ///
+ public bool IsConnected { get; private set; }
+
+ ///
+ /// Connection Status represented as a ushort
+ ///
+ public ushort UIsConnected { get { return (ushort)(Client.IsConnected ? 1 : 0); } }
+
+ ///
+ /// The client
+ ///
+ public GenericSshClient Client { get; private set; }
+
+ ///
+ /// Socket Status as represented by
+ ///
+ public ushort Status { get { return Client.UStatus; } }
+
+ ///
+ /// S+ Constructor
+ ///
+ public SshConnectionChangeEventArgs() { }
+
+ ///
+ /// EventArgs class
+ ///
+ /// Connection State
+ /// The Client
+ public SshConnectionChangeEventArgs(bool isConnected, GenericSshClient client)
+ {
+ IsConnected = isConnected;
+ Client = client;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs
new file mode 100644
index 00000000..9529aa29
--- /dev/null
+++ b/src/PepperDash.Core/Comm/GenericTcpIpClient.cs
@@ -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
+{
+ ///
+ /// A class to handle basic TCP/IP communications with a server
+ ///
+ public class GenericTcpIpClient : Device, ISocketStatusWithStreamDebugging, IAutoReconnect
+ {
+ private const string SplusKey = "Uninitialized TcpIpClient";
+ ///
+ /// Object to enable stream debugging
+ ///
+ public CommunicationStreamDebugging StreamDebugging { get; private set; }
+
+ ///
+ /// Fires when data is received from the server and returns it as a Byte array
+ ///
+ public event EventHandler BytesReceived;
+
+ ///
+ /// Fires when data is received from the server and returns it as text
+ ///
+ public event EventHandler TextReceived;
+
+ ///
+ ///
+ ///
+ //public event GenericSocketStatusChangeEventDelegate SocketStatusChange;
+ public event EventHandler ConnectionChange;
+
+
+ private string _hostname;
+
+ ///
+ /// Address of server
+ ///
+ public string Hostname
+ {
+ get
+ {
+ return _hostname;
+ }
+
+ set
+ {
+ _hostname = value;
+ if (_client != null)
+ {
+ _client.AddressClientConnectedTo = _hostname;
+ }
+ }
+ }
+
+ ///
+ /// Port on server
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// Another damn S+ helper because S+ seems to treat large port nums as signed ints
+ /// which screws up things
+ ///
+ public ushort UPort
+ {
+ get { return Convert.ToUInt16(Port); }
+ set { Port = Convert.ToInt32(value); }
+ }
+
+ ///
+ /// Defaults to 2000
+ ///
+ public int BufferSize { get; set; }
+
+ ///
+ /// The actual client class
+ ///
+ private TCPClient _client;
+
+ ///
+ /// Bool showing if socket is connected
+ ///
+ public bool IsConnected
+ {
+ get { return _client != null && _client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED; }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsConnected
+ {
+ get { return (ushort)(IsConnected ? 1 : 0); }
+ }
+
+ ///
+ /// _client socket status Read only
+ ///
+ public SocketStatus ClientStatus
+ {
+ get
+ {
+ return _client == null ? SocketStatus.SOCKET_STATUS_NO_CONNECT : _client.ClientStatus;
+ }
+ }
+
+ ///
+ /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
+ /// and IsConnected would be true when this == 2.
+ ///
+ public ushort UStatus
+ {
+ get { return (ushort)ClientStatus; }
+ }
+
+ ///
+ /// Status text shows the message associated with socket status
+ ///
+ public string ClientStatusText { get { return ClientStatus.ToString(); } }
+
+ ///
+ /// Ushort representation of client status
+ ///
+ [Obsolete]
+ public ushort UClientStatus { get { return (ushort)ClientStatus; } }
+
+ ///
+ /// Connection failure reason
+ ///
+ public string ConnectionFailure { get { return ClientStatus.ToString(); } }
+
+ ///
+ /// bool to track if auto reconnect should be set on the socket
+ ///
+ public bool AutoReconnect { get; set; }
+
+ ///
+ /// S+ helper for AutoReconnect
+ ///
+ public ushort UAutoReconnect
+ {
+ get { return (ushort)(AutoReconnect ? 1 : 0); }
+ set { AutoReconnect = value == 1; }
+ }
+
+ ///
+ /// Milliseconds to wait before attempting to reconnect. Defaults to 5000
+ ///
+ public int AutoReconnectIntervalMs { get; set; }
+
+ ///
+ /// Set only when the disconnect method is called
+ ///
+ bool DisconnectCalledByUser;
+
+ ///
+ ///
+ ///
+ 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;
+
+ ///
+ /// Constructor
+ ///
+ /// unique string to differentiate between instances
+ ///
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Default constructor for S+
+ ///
+ 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);
+ }
+
+ ///
+ /// Just to help S+ set the key
+ ///
+ public void Initialize(string key)
+ {
+ Key = key;
+ }
+
+ ///
+ /// Handles closing this up when the program shuts down
+ ///
+ void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ {
+ if (programEventType == eProgramStatusEventType.Stopping)
+ {
+ Debug.Console(1, this, "Program stopping. Closing connection");
+ Deactivate();
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override bool Deactivate()
+ {
+ RetryTimer.Stop();
+ RetryTimer.Dispose();
+ if (_client != null)
+ {
+ _client.SocketStatusChange -= this.Client_SocketStatusChange;
+ DisconnectClient();
+ }
+ return true;
+ }
+
+ ///
+ /// Attempts to connect to the server
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Attempts to disconnect the client
+ ///
+ public void Disconnect()
+ {
+ try
+ {
+ connectLock.Enter();
+ DisconnectCalledByUser = true;
+
+ // Stop trying reconnects, if we are
+ RetryTimer.Stop();
+ DisconnectClient();
+ }
+ finally
+ {
+ connectLock.Leave();
+ }
+ }
+
+ ///
+ /// Does the actual disconnect business
+ ///
+ public void DisconnectClient()
+ {
+ if (_client != null)
+ {
+ Debug.Console(1, this, "Disconnecting client");
+ if (IsConnected)
+ _client.DisconnectFromServer();
+ }
+ }
+
+ ///
+ /// Callback method for connection attempt
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Disconnects, waits and attemtps to connect again
+ ///
+ 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();
+ }
+ });
+ }
+
+ ///
+ /// Recieves incoming data
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// General send method
+ ///
+ 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);
+ }
+
+ ///
+ /// This is useful from console and...?
+ ///
+ 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);
+ }
+
+ ///
+ /// Sends Bytes to the server
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Socket Status Change Handler
+ ///
+ ///
+ ///
+ 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));
+ }
+ }
+
+ ///
+ /// Configuration properties for TCP/SSH Connections
+ ///
+ public class TcpSshPropertiesConfig
+ {
+ ///
+ /// Address to connect to
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public string Address { get; set; }
+
+ ///
+ /// Port to connect to
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public int Port { get; set; }
+
+ ///
+ /// Username credential
+ ///
+ public string Username { get; set; }
+ ///
+ /// Passord credential
+ ///
+ public string Password { get; set; }
+
+ ///
+ /// Defaults to 32768
+ ///
+ public int BufferSize { get; set; }
+
+ ///
+ /// Defaults to true
+ ///
+ public bool AutoReconnect { get; set; }
+
+ ///
+ /// Defaults to 5000ms
+ ///
+ public int AutoReconnectIntervalMs { get; set; }
+
+ ///
+ /// Default constructor
+ ///
+ public TcpSshPropertiesConfig()
+ {
+ BufferSize = 32768;
+ AutoReconnect = true;
+ AutoReconnectIntervalMs = 5000;
+ Username = "";
+ Password = "";
+ }
+
+ }
+
+}
diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs
new file mode 100644
index 00000000..03a27827
--- /dev/null
+++ b/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs
@@ -0,0 +1,775 @@
+/*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
+{
+ ///
+ /// Generic TCP/IP client for server
+ ///
+ public class GenericTcpIpClient_ForServer : Device, IAutoReconnect
+ {
+ ///
+ /// Band aid delegate for choked server
+ ///
+ internal delegate void ConnectionHasHungCallbackDelegate();
+
+ #region Events
+
+ //public event EventHandler BytesReceived;
+
+ ///
+ /// Notifies of text received
+ ///
+ public event EventHandler TextReceived;
+
+ ///
+ /// Notifies of socket status change
+ ///
+ public event EventHandler ConnectionChange;
+
+
+ ///
+ /// 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.
+ ///
+ internal ConnectionHasHungCallbackDelegate ConnectionHasHungCallback;
+
+ ///
+ /// 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.
+ ///
+ public event EventHandler ClientReadyForCommunications;
+
+ #endregion
+
+ #region Properties & Variables
+
+ ///
+ /// Address of server
+ ///
+ public string Hostname { get; set; }
+
+ ///
+ /// Port on server
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// S+ helper
+ ///
+ public ushort UPort
+ {
+ get { return Convert.ToUInt16(Port); }
+ set { Port = Convert.ToInt32(value); }
+ }
+
+ ///
+ /// Bool to show whether the server requires a preshared key. This is used in the DynamicTCPServer class
+ ///
+ public bool SharedKeyRequired { get; set; }
+
+ ///
+ /// S+ helper for requires shared key bool
+ ///
+ public ushort USharedKeyRequired
+ {
+ set
+ {
+ if (value == 1)
+ SharedKeyRequired = true;
+ else
+ SharedKeyRequired = false;
+ }
+ }
+
+ ///
+ /// 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
+ ///
+ public string SharedKey { get; set; }
+
+ ///
+ /// flag to show the client is waiting for the server to send the shared key
+ ///
+ private bool WaitingForSharedKeyResponse { get; set; }
+
+ ///
+ /// Defaults to 2000
+ ///
+ public int BufferSize { get; set; }
+
+ ///
+ /// Semaphore on connect method
+ ///
+ bool IsTryingToConnect;
+
+ ///
+ /// Bool showing if socket is connected
+ ///
+ public bool IsConnected
+ {
+ get
+ {
+ if (Client != null)
+ return Client.ClientStatus == SocketStatus.SOCKET_STATUS_CONNECTED;
+ else
+ return false;
+ }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsConnected
+ {
+ get { return (ushort)(IsConnected ? 1 : 0); }
+ }
+
+ ///
+ /// Bool showing if socket is ready for communication after shared key exchange
+ ///
+ public bool IsReadyForCommunication { get; set; }
+
+ ///
+ /// S+ helper for IsReadyForCommunication
+ ///
+ public ushort UIsReadyForCommunication
+ {
+ get { return (ushort)(IsReadyForCommunication ? 1 : 0); }
+ }
+
+ ///
+ /// Client socket status Read only
+ ///
+ public SocketStatus ClientStatus
+ {
+ get
+ {
+ if (Client != null)
+ return Client.ClientStatus;
+ else
+ return SocketStatus.SOCKET_STATUS_NO_CONNECT;
+ }
+ }
+
+ ///
+ /// Contains the familiar Simpl analog status values. This drives the ConnectionChange event
+ /// and IsConnected would be true when this == 2.
+ ///
+ public ushort UStatus
+ {
+ get { return (ushort)ClientStatus; }
+ }
+
+ ///
+ /// Status text shows the message associated with socket status
+ ///
+ public string ClientStatusText { get { return ClientStatus.ToString(); } }
+
+ ///
+ /// bool to track if auto reconnect should be set on the socket
+ ///
+ public bool AutoReconnect { get; set; }
+
+ ///
+ /// S+ helper for AutoReconnect
+ ///
+ public ushort UAutoReconnect
+ {
+ get { return (ushort)(AutoReconnect ? 1 : 0); }
+ set { AutoReconnect = value == 1; }
+ }
+ ///
+ /// Milliseconds to wait before attempting to reconnect. Defaults to 5000
+ ///
+ public int AutoReconnectIntervalMs { get; set; }
+
+ ///
+ /// Flag Set only when the disconnect method is called.
+ ///
+ bool DisconnectCalledByUser;
+
+ ///
+ /// private Timer for auto reconnect
+ ///
+ CTimer RetryTimer;
+
+
+ ///
+ ///
+ ///
+ public bool HeartbeatEnabled { get; set; }
+
+ ///
+ ///
+ ///
+ public ushort UHeartbeatEnabled
+ {
+ get { return (ushort)(HeartbeatEnabled ? 1 : 0); }
+ set { HeartbeatEnabled = value == 1; }
+ }
+
+ ///
+ ///
+ ///
+ public string HeartbeatString = "heartbeat";
+
+ ///
+ ///
+ ///
+ public int HeartbeatInterval = 50000;
+
+ CTimer HeartbeatSendTimer;
+ CTimer HeartbeatAckTimer;
+ ///
+ /// Used to force disconnection on a dead connect attempt
+ ///
+ CTimer ConnectFailTimer;
+ CTimer WaitForSharedKey;
+ private int ConnectionCount;
+ ///
+ /// Internal secure client
+ ///
+ TCPClient Client;
+
+ bool ProgramIsStopping;
+
+ #endregion
+
+ #region Constructors
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+
+ }
+
+ ///
+ /// Constructor for S+
+ ///
+ public GenericTcpIpClient_ForServer()
+ : base("Uninitialized DynamicTcpClient")
+ {
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ AutoReconnectIntervalMs = 5000;
+ BufferSize = 2000;
+ }
+ #endregion
+
+ #region Methods
+
+ ///
+ /// Just to help S+ set the key
+ ///
+ public void Initialize(string key)
+ {
+ Key = key;
+ }
+
+ ///
+ /// Handles closing this up when the program shuts down
+ ///
+ 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();
+ }
+
+ }
+
+ ///
+ /// Connect Method. Will return if already connected. Will write errors if missing address, port, or unique key/name.
+ ///
+ 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();
+ }
+ }
+
+ ///
+ ///
+ ///
+ public void Disconnect()
+ {
+ this.LogVerbose("Disconnect Called");
+
+ DisconnectCalledByUser = true;
+ if (IsConnected)
+ {
+ Client.DisconnectFromServer();
+
+ }
+ if (RetryTimer != null)
+ {
+ RetryTimer.Stop();
+ RetryTimer = null;
+ }
+ Cleanup();
+ }
+
+ ///
+ /// Internal call to close up client. ALWAYS use this when disconnecting.
+ ///
+ 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;
+ }
+ }
+
+
+ /// ff
+ /// Called from Connect failure or Socket Status change if
+ /// auto reconnect and socket disconnected (Not disconnected by user)
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Receive callback
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ ///
+ ///
+ void StopWaitForSharedKeyTimer()
+ {
+ if (WaitForSharedKey != null)
+ {
+ WaitForSharedKey.Stop();
+ WaitForSharedKey = null;
+ }
+ }
+
+ ///
+ /// General send method
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// SocketStatusChange Callback
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Helper for ConnectionChange event
+ ///
+ void OnConnectionChange()
+ {
+ var handler = ConnectionChange;
+ if (handler != null)
+ ConnectionChange(this, new GenericTcpServerSocketStatusChangeEventArgs(this, Client.ClientStatus));
+ }
+
+ ///
+ /// Helper to fire ClientReadyForCommunications event
+ ///
+ void OnClientReadyForcommunications(bool isReady)
+ {
+ IsReadyForCommunication = isReady;
+ if (this.IsReadyForCommunication) { HeartbeatStart(); }
+ var handler = ClientReadyForCommunications;
+ if (handler != null)
+ handler(this, new GenericTcpServerClientReadyForcommunicationsEventArgs(IsReadyForCommunication));
+ }
+ #endregion
+ }
+
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/GenericTcpIpServer.cs b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs
new file mode 100644
index 00000000..6aa5e6b5
--- /dev/null
+++ b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs
@@ -0,0 +1,1011 @@
+/*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 Crestron.SimplSharp;
+using Crestron.SimplSharp.CrestronSockets;
+using PepperDash.Core.Logging;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Generic TCP/IP server device
+ ///
+ public class GenericTcpIpServer : Device
+ {
+ #region Events
+ ///
+ /// Event for Receiving text
+ ///
+ public event EventHandler TextReceived;
+
+ ///
+ /// Event for client connection socket status change
+ ///
+ public event EventHandler ClientConnectionChange;
+
+ ///
+ /// Event for Server State Change
+ ///
+ public event EventHandler ServerStateChange;
+
+ ///
+ /// For a server with a pre shared key, this will fire after the communication is established and the key exchange is complete. If no shared key, this will fire
+ /// after connection is successful. Use this event to know when the client is ready for communication to avoid stepping on shared key.
+ ///
+ public event EventHandler ServerClientReadyForCommunications;
+
+ ///
+ /// A band aid event to notify user that the server has choked.
+ ///
+ public ServerHasChokedCallbackDelegate ServerHasChoked { get; set; }
+
+ ///
+ ///
+ ///
+ public delegate void ServerHasChokedCallbackDelegate();
+
+ #endregion
+
+ #region Properties/Variables
+
+ ///
+ ///
+ ///
+ CCriticalSection ServerCCSection = new CCriticalSection();
+
+
+ ///
+ /// A bandaid client that monitors whether the server is reachable
+ ///
+ GenericTcpIpClient_ForServer MonitorClient;
+
+ ///
+ /// Timer to operate the bandaid monitor client in a loop.
+ ///
+ CTimer MonitorClientTimer;
+
+ ///
+ ///
+ ///
+ int MonitorClientFailureCount;
+
+ ///
+ /// 3 by default
+ ///
+ public int MonitorClientMaxFailureCount { get; set; }
+
+ ///
+ /// Text representation of the Socket Status enum values for the server
+ ///
+ public string Status
+ {
+ get
+ {
+ if (myTcpServer != null)
+ return myTcpServer.State.ToString();
+ return ServerState.SERVER_NOT_LISTENING.ToString();
+
+ }
+
+ }
+
+ ///
+ /// Bool showing if socket is connected
+ ///
+ public bool IsConnected
+ {
+ get
+ {
+ if (myTcpServer != null)
+ return (myTcpServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED;
+ return false;
+
+ //return (Secure ? SecureServer != null : UnsecureServer != null) &&
+ //(Secure ? (SecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED :
+ // (UnsecureServer.State & ServerState.SERVER_CONNECTED) == ServerState.SERVER_CONNECTED);
+ }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsConnected
+ {
+ get { return (ushort)(IsConnected ? 1 : 0); }
+ }
+
+ ///
+ /// Bool showing if socket is connected
+ ///
+ public bool IsListening
+ {
+ get
+ {
+ if (myTcpServer != null)
+ return (myTcpServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING;
+ else
+ return false;
+ //return (Secure ? SecureServer != null : UnsecureServer != null) &&
+ //(Secure ? (SecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING :
+ // (UnsecureServer.State & ServerState.SERVER_LISTENING) == ServerState.SERVER_LISTENING);
+ }
+ }
+
+ ///
+ /// S+ helper for IsConnected
+ ///
+ public ushort UIsListening
+ {
+ get { return (ushort)(IsListening ? 1 : 0); }
+ }
+
+ ///
+ /// The maximum number of clients.
+ /// Should be set by parameter in SIMPL+ in the MAIN method, Should not ever need to be configurable
+ ///
+ public ushort MaxClients { get; set; }
+
+ ///
+ /// Number of clients currently connected.
+ ///
+ public ushort NumberOfClientsConnected
+ {
+ get
+ {
+ if (myTcpServer != null)
+ return (ushort)myTcpServer.NumberOfClientsConnected;
+ return 0;
+ }
+ }
+
+ ///
+ /// Port Server should listen on
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// S+ helper for Port
+ ///
+ public ushort UPort
+ {
+ get { return Convert.ToUInt16(Port); }
+ set { Port = Convert.ToInt32(value); }
+ }
+
+ ///
+ /// Bool to show whether the server requires a preshared key. Must be set the same in the client, and if true shared keys must be identical on server/client
+ ///
+ public bool SharedKeyRequired { get; set; }
+
+ ///
+ /// S+ helper for requires shared key bool
+ ///
+ public ushort USharedKeyRequired
+ {
+ set
+ {
+ if (value == 1)
+ SharedKeyRequired = true;
+ else
+ SharedKeyRequired = false;
+ }
+ }
+
+ ///
+ /// 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.
+ /// If SharedKey changes while server is listening or clients are connected, disconnect and stop listening will be called
+ ///
+ public string SharedKey { get; set; }
+
+ ///
+ /// Heartbeat Required bool sets whether server disconnects client if heartbeat is not received
+ ///
+ public bool HeartbeatRequired { get; set; }
+
+ ///
+ /// S+ Helper for Heartbeat Required
+ ///
+ public ushort UHeartbeatRequired
+ {
+ set
+ {
+ if (value == 1)
+ HeartbeatRequired = true;
+ else
+ HeartbeatRequired = false;
+ }
+ }
+
+ ///
+ /// Milliseconds before server expects another heartbeat. Set by property HeartbeatRequiredIntervalInSeconds which is driven from S+
+ ///
+ public int HeartbeatRequiredIntervalMs { get; set; }
+
+ ///
+ /// Simpl+ Heartbeat Analog value in seconds
+ ///
+ public ushort HeartbeatRequiredIntervalInSeconds { set { HeartbeatRequiredIntervalMs = (value * 1000); } }
+
+ ///
+ /// String to Match for heartbeat. If null or empty any string will reset heartbeat timer
+ ///
+ public string HeartbeatStringToMatch { get; set; }
+
+ //private timers for Heartbeats per client
+ Dictionary HeartbeatTimerDictionary = new Dictionary();
+
+ //flags to show the secure server is waiting for client at index to send the shared key
+ List WaitingForSharedKey = new List();
+
+ List ClientReadyAfterKeyExchange = new List();
+
+ ///
+ /// The connected client indexes
+ ///
+ public List ConnectedClientsIndexes = new List();
+
+ ///
+ /// Defaults to 2000
+ ///
+ public int BufferSize { get; set; }
+
+ ///
+ /// Private flag to note that the server has stopped intentionally
+ ///
+ private bool ServerStopped { get; set; }
+
+ //Servers
+ TCPServer myTcpServer;
+
+ ///
+ ///
+ ///
+ bool ProgramIsStopping;
+
+ #endregion
+
+ #region Constructors
+ ///
+ /// constructor S+ Does not accept a key. Use initialze with key to set the debug key on this device. If using with + make sure to set all properties manually.
+ ///
+ public GenericTcpIpServer()
+ : base("Uninitialized Dynamic TCP Server")
+ {
+ HeartbeatRequiredIntervalInSeconds = 15;
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ BufferSize = 2000;
+ MonitorClientMaxFailureCount = 3;
+ }
+
+ ///
+ /// constructor with debug key set at instantiation. Make sure to set all properties before listening.
+ ///
+ ///
+ public GenericTcpIpServer(string key)
+ : base("Uninitialized Dynamic TCP Server")
+ {
+ HeartbeatRequiredIntervalInSeconds = 15;
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ BufferSize = 2000;
+ MonitorClientMaxFailureCount = 3;
+ Key = key;
+ }
+
+ ///
+ /// Contstructor that sets all properties by calling the initialize method with a config object.
+ ///
+ ///
+ public GenericTcpIpServer(TcpServerConfigObject serverConfigObject)
+ : base("Uninitialized Dynamic TCP Server")
+ {
+ HeartbeatRequiredIntervalInSeconds = 15;
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ BufferSize = 2000;
+ MonitorClientMaxFailureCount = 3;
+ Initialize(serverConfigObject);
+ }
+ #endregion
+
+ #region Methods - Server Actions
+ ///
+ /// Disconnects all clients and stops the server
+ ///
+ public void KillServer()
+ {
+ ServerStopped = true;
+ if (MonitorClient != null)
+ {
+ MonitorClient.Disconnect();
+ }
+ DisconnectAllClientsForShutdown();
+ StopListening();
+ }
+
+ ///
+ /// Initialize Key for device using client name from SIMPL+. Called on Listen from SIMPL+
+ ///
+ ///
+ public void Initialize(string key)
+ {
+ Key = key;
+ }
+
+ ///
+ /// Initialze with server configuration object
+ ///
+ ///
+ public void Initialize(TcpServerConfigObject serverConfigObject)
+ {
+ try
+ {
+ if (serverConfigObject != null || string.IsNullOrEmpty(serverConfigObject.Key))
+ {
+ Key = serverConfigObject.Key;
+ MaxClients = serverConfigObject.MaxClients;
+ Port = serverConfigObject.Port;
+ SharedKeyRequired = serverConfigObject.SharedKeyRequired;
+ SharedKey = serverConfigObject.SharedKey;
+ HeartbeatRequired = serverConfigObject.HeartbeatRequired;
+ HeartbeatRequiredIntervalInSeconds = serverConfigObject.HeartbeatRequiredIntervalInSeconds;
+ HeartbeatStringToMatch = serverConfigObject.HeartbeatStringToMatch;
+ BufferSize = serverConfigObject.BufferSize;
+
+ }
+ else
+ {
+ ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key);
+ }
+ }
+ catch
+ {
+ ErrorLog.Error("Could not initialize server with key: {0}", serverConfigObject.Key);
+ }
+ }
+
+ ///
+ /// Start listening on the specified port
+ ///
+ public void Listen()
+ {
+ ServerCCSection.Enter();
+ try
+ {
+ if (Port < 1 || Port > 65535)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': Invalid port", Key);
+ ErrorLog.Warn(string.Format("Server '{0}': Invalid port", Key));
+ return;
+ }
+ if (string.IsNullOrEmpty(SharedKey) && SharedKeyRequired)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Server '{0}': No Shared Key set", Key);
+ ErrorLog.Warn(string.Format("Server '{0}': No Shared Key set", Key));
+ return;
+ }
+ if (IsListening)
+ return;
+
+ if (myTcpServer == null)
+ {
+ myTcpServer = new TCPServer(Port, MaxClients);
+ if(HeartbeatRequired)
+ myTcpServer.SocketSendOrReceiveTimeOutInMs = (this.HeartbeatRequiredIntervalMs * 5);
+
+ // myTcpServer.HandshakeTimeout = 30;
+ }
+ else
+ {
+ KillServer();
+ myTcpServer.PortNumber = Port;
+ }
+
+ myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange;
+ myTcpServer.SocketStatusChange += TcpServer_SocketStatusChange;
+
+ ServerStopped = false;
+ myTcpServer.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
+ OnServerStateChange(myTcpServer.State);
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "TCP Server Status: {0}, Socket Status: {1}", myTcpServer.State, myTcpServer.ServerSocketStatus);
+
+ // StartMonitorClient();
+
+
+ ServerCCSection.Leave();
+ }
+ catch (Exception ex)
+ {
+ ServerCCSection.Leave();
+ ErrorLog.Error("{1} Error with Dynamic Server: {0}", ex.ToString(), Key);
+ }
+ }
+
+ ///
+ /// Stop Listening
+ ///
+ public void StopListening()
+ {
+ try
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Stopping Listener");
+ if (myTcpServer != null)
+ {
+ myTcpServer.Stop();
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server State: {0}", myTcpServer.State);
+ OnServerStateChange(myTcpServer.State);
+ }
+ ServerStopped = true;
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error stopping server. Error: {0}", ex);
+ }
+ }
+
+ ///
+ /// Disconnects Client
+ ///
+ ///
+ public void DisconnectClient(uint client)
+ {
+ try
+ {
+ myTcpServer.Disconnect(client);
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", client);
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", client, ex);
+ }
+ }
+ ///
+ /// Disconnect All Clients
+ ///
+ public void DisconnectAllClientsForShutdown()
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Disconnecting All Clients");
+ if (myTcpServer != null)
+ {
+ myTcpServer.SocketStatusChange -= TcpServer_SocketStatusChange;
+ foreach (var index in ConnectedClientsIndexes.ToList()) // copy it here so that it iterates properly
+ {
+ var i = index;
+ if (!myTcpServer.ClientConnected(index))
+ continue;
+ try
+ {
+ myTcpServer.Disconnect(i);
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected client index: {0}", i);
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Disconnecting client index: {0}. Error: {1}", i, ex);
+ }
+ }
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Server Status: {0}", myTcpServer.ServerSocketStatus);
+ }
+
+ Debug.Console(2, this, Debug.ErrorLogLevel.Notice, "Disconnected All Clients");
+ ConnectedClientsIndexes.Clear();
+
+ if (!ProgramIsStopping)
+ {
+ OnConnectionChange();
+ OnServerStateChange(myTcpServer.State); //State shows both listening and connected
+ }
+
+ // var o = new { };
+ }
+
+ ///
+ /// Broadcast text from server to all connected clients
+ ///
+ ///
+ public void BroadcastText(string text)
+ {
+ CCriticalSection CCBroadcast = new CCriticalSection();
+ CCBroadcast.Enter();
+ try
+ {
+ if (ConnectedClientsIndexes.Count > 0)
+ {
+ byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
+ foreach (uint i in ConnectedClientsIndexes)
+ {
+ if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(i)))
+ {
+ SocketErrorCodes error = myTcpServer.SendDataAsync(i, b, b.Length, (x, y, z) => { });
+ if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING)
+ this.LogError("{error}",error.ToString());
+ }
+ }
+ }
+ CCBroadcast.Leave();
+ }
+ catch (Exception ex)
+ {
+ CCBroadcast.Leave();
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Broadcasting messages from server. Error: {0}", ex.Message);
+ }
+ }
+
+ ///
+ /// Not sure this is useful in library, maybe Pro??
+ ///
+ ///
+ ///
+ public void SendTextToClient(string text, uint clientIndex)
+ {
+ try
+ {
+ byte[] b = Encoding.GetEncoding(28591).GetBytes(text);
+ if (myTcpServer != null && myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
+ {
+ if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
+ myTcpServer.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, "Error sending text to client. Text: {1}. Error: {0}", ex.Message, text);
+ }
+ }
+
+ //private method to check heartbeat requirements and start or reset timer
+ string checkHeartbeat(uint clientIndex, string received)
+ {
+ try
+ {
+ if (HeartbeatRequired)
+ {
+ if (!string.IsNullOrEmpty(HeartbeatStringToMatch))
+ {
+ var remainingText = received.Replace(HeartbeatStringToMatch, "");
+ var noDelimiter = received.Trim(new char[] { '\r', '\n' });
+ if (noDelimiter.Contains(HeartbeatStringToMatch))
+ {
+ if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
+ else
+ {
+ CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
+ HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
+ }
+ Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", HeartbeatStringToMatch, clientIndex);
+ // Return Heartbeat
+ SendTextToClient(HeartbeatStringToMatch, clientIndex);
+ return remainingText;
+ }
+ }
+ else
+ {
+ if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ HeartbeatTimerDictionary[clientIndex].Reset(HeartbeatRequiredIntervalMs);
+ else
+ {
+ CTimer HeartbeatTimer = new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs);
+ HeartbeatTimerDictionary.Add(clientIndex, HeartbeatTimer);
+ }
+ Debug.Console(1, this, "Heartbeat Received: {0}, from client index: {1}", received, clientIndex);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message);
+ }
+ return received;
+ }
+
+ ///
+ /// Gets the IP address based on the client index
+ ///
+ ///
+ /// IP address of the client
+ public string GetClientIPAddress(uint clientIndex)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress Index: {0}", clientIndex);
+ if (!SharedKeyRequired || (SharedKeyRequired && ClientReadyAfterKeyExchange.Contains(clientIndex)))
+ {
+ var ipa = this.myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "GetClientIPAddress IPAddreess: {0}", ipa);
+ return ipa;
+
+ }
+ else
+ {
+ return "";
+ }
+ }
+
+ #endregion
+
+ #region Methods - HeartbeatTimer Callback
+
+ void HeartbeatTimer_CallbackFunction(object o)
+ {
+ uint clientIndex = 99999;
+ string address = string.Empty;
+ try
+ {
+ clientIndex = (uint)o;
+ address = myTcpServer.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex);
+
+ Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Heartbeat not received for Client index {2} IP: {0}, DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE {1}",
+ address, string.IsNullOrEmpty(HeartbeatStringToMatch) ? "" : ("HeartbeatStringToMatch: " + HeartbeatStringToMatch), clientIndex);
+
+ if (myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
+ SendTextToClient("Heartbeat not received by server, closing connection", clientIndex);
+
+ var discoResult = myTcpServer.Disconnect(clientIndex);
+ //Debug.Console(1, this, "{0}", discoResult);
+
+ if (HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ {
+ HeartbeatTimerDictionary[clientIndex].Stop();
+ HeartbeatTimerDictionary[clientIndex].Dispose();
+ HeartbeatTimerDictionary.Remove(clientIndex);
+ }
+ }
+ catch (Exception ex)
+ {
+ ErrorLog.Error("{3}: Heartbeat timeout Error on Client Index: {0}, at address: {1}, error: {2}", clientIndex, address, ex.Message, Key);
+ }
+ }
+
+ #endregion
+
+ #region Methods - Socket Status Changed Callbacks
+ ///
+ /// Secure Server Socket Status Changed Callback
+ ///
+ ///
+ ///
+ ///
+ void TcpServer_SocketStatusChange(TCPServer server, uint clientIndex, SocketStatus serverSocketStatus)
+ {
+ try
+ {
+
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SecureServerSocketStatusChange Index:{0} status:{1} Port:{2} IP:{3}", clientIndex, serverSocketStatus, this.myTcpServer.GetPortNumberServerAcceptedConnectionFromForSpecificClient(clientIndex), this.myTcpServer.GetLocalAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
+ if (serverSocketStatus != SocketStatus.SOCKET_STATUS_CONNECTED)
+ {
+ if (ConnectedClientsIndexes.Contains(clientIndex))
+ ConnectedClientsIndexes.Remove(clientIndex);
+ if (HeartbeatRequired && HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ {
+ HeartbeatTimerDictionary[clientIndex].Stop();
+ HeartbeatTimerDictionary[clientIndex].Dispose();
+ HeartbeatTimerDictionary.Remove(clientIndex);
+ }
+ if (ClientReadyAfterKeyExchange.Contains(clientIndex))
+ ClientReadyAfterKeyExchange.Remove(clientIndex);
+ if (WaitingForSharedKey.Contains(clientIndex))
+ WaitingForSharedKey.Remove(clientIndex);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Change Callback. Error: {0}", ex);
+ }
+ onConnectionChange(clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
+ }
+
+ #endregion
+
+ #region Methods Connected Callbacks
+ ///
+ /// Secure TCP Client Connected to Secure Server Callback
+ ///
+ ///
+ ///
+ void TcpConnectCallback(TCPServer server, uint clientIndex)
+ {
+ try
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "ConnectCallback: IPAddress: {0}. Index: {1}. Status: {2}",
+ server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex),
+ clientIndex, server.GetServerSocketStatusForSpecificClient(clientIndex));
+ if (clientIndex != 0)
+ {
+ if (server.ClientConnected(clientIndex))
+ {
+
+ if (!ConnectedClientsIndexes.Contains(clientIndex))
+ {
+ ConnectedClientsIndexes.Add(clientIndex);
+ }
+ if (SharedKeyRequired)
+ {
+ if (!WaitingForSharedKey.Contains(clientIndex))
+ {
+ WaitingForSharedKey.Add(clientIndex);
+ }
+ byte[] b = Encoding.GetEncoding(28591).GetBytes("SharedKey:");
+ server.SendDataAsync(clientIndex, b, b.Length, (x, y, z) => { });
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Sent Shared Key Request to client at {0}", server.GetAddressServerAcceptedConnectionFromForSpecificClient(clientIndex));
+ }
+ else
+ {
+ OnServerClientReadyForCommunications(clientIndex);
+ }
+ if (HeartbeatRequired)
+ {
+ if (!HeartbeatTimerDictionary.ContainsKey(clientIndex))
+ {
+ HeartbeatTimerDictionary.Add(clientIndex, new CTimer(HeartbeatTimer_CallbackFunction, clientIndex, HeartbeatRequiredIntervalMs));
+ }
+ }
+
+ server.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback);
+ }
+ }
+ else
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Error, "Client attempt faulty.");
+ if (!ServerStopped)
+ {
+ server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
+ return;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error in Socket Status Connect Callback. Error: {0}", ex);
+ }
+ //Debug.Console(1, this, Debug.ErrorLogLevel, "((((((Server State bitfield={0}; maxclient={1}; ServerStopped={2}))))))",
+ // server.State,
+ // MaxClients,
+ // ServerStopped);
+ if ((server.State & ServerState.SERVER_LISTENING) != ServerState.SERVER_LISTENING && MaxClients > 1 && !ServerStopped)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Waiting for next connection");
+ server.WaitForConnectionAsync(IPAddress.Any, TcpConnectCallback);
+
+ }
+ }
+
+ #endregion
+
+ #region Methods - Send/Receive Callbacks
+ ///
+ /// Secure Received Data Async Callback
+ ///
+ ///
+ ///
+ ///
+ void TcpServerReceivedDataAsyncCallback(TCPServer myTCPServer, uint clientIndex, int numberOfBytesReceived)
+ {
+ if (numberOfBytesReceived > 0)
+ {
+ string received = "Nothing";
+ try
+ {
+ byte[] bytes = myTCPServer.GetIncomingDataBufferForSpecificClient(clientIndex);
+ received = System.Text.Encoding.GetEncoding(28591).GetString(bytes, 0, numberOfBytesReceived);
+ if (WaitingForSharedKey.Contains(clientIndex))
+ {
+ received = received.Replace("\r", "");
+ received = received.Replace("\n", "");
+ if (received != SharedKey)
+ {
+ byte[] b = Encoding.GetEncoding(28591).GetBytes("Shared key did not match server. Disconnecting");
+ Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Client at index {0} Shared key did not match the server, disconnecting client. Key: {1}", clientIndex, received);
+ myTCPServer.SendData(clientIndex, b, b.Length);
+ myTCPServer.Disconnect(clientIndex);
+ return;
+ }
+
+ WaitingForSharedKey.Remove(clientIndex);
+ byte[] success = Encoding.GetEncoding(28591).GetBytes("Shared Key Match");
+ myTCPServer.SendDataAsync(clientIndex, success, success.Length, null);
+ OnServerClientReadyForCommunications(clientIndex);
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Client with index {0} provided the shared key and successfully connected to the server", clientIndex);
+ }
+
+ else if (!string.IsNullOrEmpty(checkHeartbeat(clientIndex, received)))
+ onTextReceived(received, clientIndex);
+ }
+ catch (Exception ex)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error, "Error Receiving data: {0}. Error: {1}", received, ex);
+ }
+ if (myTCPServer.GetServerSocketStatusForSpecificClient(clientIndex) == SocketStatus.SOCKET_STATUS_CONNECTED)
+ myTCPServer.ReceiveDataAsync(clientIndex, TcpServerReceivedDataAsyncCallback);
+ }
+ else
+ {
+ // If numberOfBytesReceived <= 0
+ myTCPServer.Disconnect();
+ }
+
+ }
+
+ #endregion
+
+ #region Methods - EventHelpers/Callbacks
+
+ //Private Helper method to call the Connection Change Event
+ void onConnectionChange(uint clientIndex, SocketStatus clientStatus)
+ {
+ if (clientIndex != 0) //0 is error not valid client change
+ {
+ var handler = ClientConnectionChange;
+ if (handler != null)
+ {
+ handler(this, new GenericTcpServerSocketStatusChangeEventArgs(myTcpServer, clientIndex, clientStatus));
+ }
+ }
+ }
+
+ //Private Helper method to call the Connection Change Event
+ void OnConnectionChange()
+ {
+ if (ProgramIsStopping)
+ {
+ return;
+ }
+ var handler = ClientConnectionChange;
+ if (handler != null)
+ {
+ handler(this, new GenericTcpServerSocketStatusChangeEventArgs());
+ }
+ }
+
+ //Private Helper Method to call the Text Received Event
+ void onTextReceived(string text, uint clientIndex)
+ {
+ var handler = TextReceived;
+ if (handler != null)
+ handler(this, new GenericTcpServerCommMethodReceiveTextArgs(text, clientIndex));
+ }
+
+ //Private Helper Method to call the Server State Change Event
+ void OnServerStateChange(ServerState state)
+ {
+ if (ProgramIsStopping)
+ {
+ return;
+ }
+ var handler = ServerStateChange;
+ if (handler != null)
+ {
+ handler(this, new GenericTcpServerStateChangedEventArgs(state));
+ }
+ }
+
+ ///
+ /// Private Event Handler method to handle the closing of connections when the program stops
+ ///
+ ///
+ void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ {
+ if (programEventType == eProgramStatusEventType.Stopping)
+ {
+ ProgramIsStopping = true;
+ // kill bandaid things
+ if (MonitorClientTimer != null)
+ MonitorClientTimer.Stop();
+ if (MonitorClient != null)
+ MonitorClient.Disconnect();
+
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Program stopping. Closing server");
+ KillServer();
+ }
+ }
+
+ //Private event handler method to raise the event that the server is ready to send data after a successful client shared key negotiation
+ void OnServerClientReadyForCommunications(uint clientIndex)
+ {
+ ClientReadyAfterKeyExchange.Add(clientIndex);
+ var handler = ServerClientReadyForCommunications;
+ if (handler != null)
+ handler(this, new GenericTcpServerSocketStatusChangeEventArgs(
+ this, clientIndex, myTcpServer.GetServerSocketStatusForSpecificClient(clientIndex)));
+ }
+ #endregion
+
+ #region Monitor Client
+ ///
+ /// Starts the monitor client cycle. Timed wait, then call RunMonitorClient
+ ///
+ void StartMonitorClient()
+ {
+ if (MonitorClientTimer != null)
+ {
+ return;
+ }
+ MonitorClientTimer = new CTimer(o => RunMonitorClient(), 60000);
+ }
+
+ ///
+ ///
+ ///
+ void RunMonitorClient()
+ {
+ MonitorClient = new GenericTcpIpClient_ForServer(Key + "-MONITOR", "127.0.0.1", Port, 2000);
+ MonitorClient.SharedKeyRequired = this.SharedKeyRequired;
+ MonitorClient.SharedKey = this.SharedKey;
+ MonitorClient.ConnectionHasHungCallback = MonitorClientHasHungCallback;
+ //MonitorClient.ConnectionChange += MonitorClient_ConnectionChange;
+ MonitorClient.ClientReadyForCommunications += MonitorClient_IsReadyForComm;
+
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Starting monitor check");
+
+ MonitorClient.Connect();
+ // From here MonitorCLient either connects or hangs, MonitorClient will call back
+
+ }
+
+ ///
+ ///
+ ///
+ void StopMonitorClient()
+ {
+ if (MonitorClient == null)
+ return;
+
+ MonitorClient.ClientReadyForCommunications -= MonitorClient_IsReadyForComm;
+ MonitorClient.Disconnect();
+ MonitorClient = null;
+ }
+
+ ///
+ /// On monitor connect, restart the operation
+ ///
+ void MonitorClient_IsReadyForComm(object sender, GenericTcpServerClientReadyForcommunicationsEventArgs args)
+ {
+ if (args.IsReady)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Monitor client connection success. Disconnecting in 2s");
+ MonitorClientTimer.Stop();
+ MonitorClientTimer = null;
+ MonitorClientFailureCount = 0;
+ CrestronEnvironment.Sleep(2000);
+ StopMonitorClient();
+ StartMonitorClient();
+ }
+ }
+
+ ///
+ /// If the client hangs, add to counter and maybe fire the choke event
+ ///
+ void MonitorClientHasHungCallback()
+ {
+ MonitorClientFailureCount++;
+ MonitorClientTimer.Stop();
+ MonitorClientTimer = null;
+ StopMonitorClient();
+ if (MonitorClientFailureCount < MonitorClientMaxFailureCount)
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Warning, "Monitor client connection has hung {0} time{1}, maximum {2}",
+ MonitorClientFailureCount, MonitorClientFailureCount > 1 ? "s" : "", MonitorClientMaxFailureCount);
+ StartMonitorClient();
+ }
+ else
+ {
+ Debug.Console(2, this, Debug.ErrorLogLevel.Error,
+ "\r***************************\rMonitor client connection has hung a maximum of {0} times.\r***************************",
+ MonitorClientMaxFailureCount);
+
+ var handler = ServerHasChoked;
+ if (handler != null)
+ handler();
+ // Some external thing is in charge here. Expected reset of program
+ }
+ }
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/GenericUdpServer.cs b/src/PepperDash.Core/Comm/GenericUdpServer.cs
new file mode 100644
index 00000000..a5a68c45
--- /dev/null
+++ b/src/PepperDash.Core/Comm/GenericUdpServer.cs
@@ -0,0 +1,396 @@
+
+using System;
+using System.Linq;
+using System.Text;
+
+using Crestron.SimplSharp;
+using Crestron.SimplSharp.CrestronSockets;
+using Newtonsoft.Json;
+using PepperDash.Core.Logging;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Generic UDP Server device
+ ///
+ public class GenericUdpServer : Device, ISocketStatusWithStreamDebugging
+ {
+ private const string SplusKey = "Uninitialized Udp Server";
+ ///
+ /// Object to enable stream debugging
+ ///
+ public CommunicationStreamDebugging StreamDebugging { get; private set; }
+ ///
+ ///
+ ///
+ public event EventHandler BytesReceived;
+
+ ///
+ ///
+ ///
+ public event EventHandler TextReceived;
+
+ ///
+ /// 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.
+ ///
+ public event EventHandler DataRecievedExtra;
+
+ ///
+ ///
+ ///
+ public event EventHandler ConnectionChange;
+
+ ///
+ ///
+ ///
+ public event EventHandler UpdateConnectionStatus;
+
+ ///
+ ///
+ ///
+ public SocketStatus ClientStatus
+ {
+ get
+ {
+ return Server.ServerStatus;
+ }
+ }
+
+ ///
+ ///
+ ///
+ public ushort UStatus
+ {
+ get { return (ushort)Server.ServerStatus; }
+ }
+
+ ///
+ /// Address of server
+ ///
+ public string Hostname { get; set; }
+
+
+ ///
+ /// Port on server
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// Another damn S+ helper because S+ seems to treat large port nums as signed ints
+ /// which screws up things
+ ///
+ public ushort UPort
+ {
+ get { return Convert.ToUInt16(Port); }
+ set { Port = Convert.ToInt32(value); }
+ }
+
+ ///
+ /// Indicates that the UDP Server is enabled
+ ///
+ public bool IsConnected
+ {
+ get;
+ private set;
+ }
+
+ ///
+ /// Numeric value indicating
+ ///
+ public ushort UIsConnected
+ {
+ get { return IsConnected ? (ushort)1 : (ushort)0; }
+ }
+
+ ///
+ /// Defaults to 2000
+ ///
+ public int BufferSize { get; set; }
+
+ ///
+ /// The server
+ ///
+ public UDPServer Server { get; private set; }
+
+ ///
+ /// Constructor for S+. Make sure to set key, address, port, and buffersize using init method
+ ///
+ public GenericUdpServer()
+ : base(SplusKey)
+ {
+ StreamDebugging = new CommunicationStreamDebugging(SplusKey);
+ BufferSize = 5000;
+
+ CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler);
+ CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Call from S+ to initialize values
+ ///
+ ///
+ ///
+ ///
+ public void Initialize(string key, string address, ushort port)
+ {
+ Key = key;
+ Hostname = address;
+ UPort = port;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ 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();
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ {
+ if (programEventType != eProgramStatusEventType.Stopping)
+ return;
+
+ Debug.Console(1, this, "Program stopping. Disabling Server");
+ Disconnect();
+ }
+
+ ///
+ /// Enables the UDP Server
+ ///
+ 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);
+ }
+
+ ///
+ /// Disabled the UDP Server
+ ///
+ public void Disconnect()
+ {
+ if(Server != null)
+ Server.DisableUDPServer();
+
+ IsConnected = false;
+
+ var handler = UpdateConnectionStatus;
+ if (handler != null)
+ handler(this, new GenericUdpConnectedEventArgs(UIsConnected));
+ }
+
+
+ ///
+ /// Recursive method to receive data
+ ///
+ ///
+ ///
+ 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)
+ {
+ this.LogException(ex, "GenericUdpServer Receive error");
+ }
+ finally
+ {
+ server.ReceiveDataAsync(Receive);
+ }
+ }
+
+ ///
+ /// General send method
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+
+ }
+
+ ///
+ ///
+ ///
+ public class GenericUdpReceiveTextExtraArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public string Text { get; private set; }
+ ///
+ ///
+ ///
+ public string IpAddress { get; private set; }
+ ///
+ ///
+ ///
+ public int Port { get; private set; }
+ ///
+ ///
+ ///
+ public byte[] Bytes { get; private set; }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public GenericUdpReceiveTextExtraArgs(string text, string ipAddress, int port, byte[] bytes)
+ {
+ Text = text;
+ IpAddress = ipAddress;
+ Port = port;
+ Bytes = bytes;
+ }
+
+ ///
+ /// Stupid S+ Constructor
+ ///
+ public GenericUdpReceiveTextExtraArgs() { }
+ }
+
+ ///
+ ///
+ ///
+ public class UdpServerPropertiesConfig
+ {
+ ///
+ ///
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public string Address { get; set; }
+
+ ///
+ ///
+ ///
+ [JsonProperty(Required = Required.Always)]
+ public int Port { get; set; }
+
+ ///
+ /// Defaults to 32768
+ ///
+ public int BufferSize { get; set; }
+
+ ///
+ ///
+ ///
+ public UdpServerPropertiesConfig()
+ {
+ BufferSize = 32768;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/TcpClientConfigObject.cs b/src/PepperDash.Core/Comm/TcpClientConfigObject.cs
new file mode 100644
index 00000000..c3b3bcec
--- /dev/null
+++ b/src/PepperDash.Core/Comm/TcpClientConfigObject.cs
@@ -0,0 +1,59 @@
+using Newtonsoft.Json;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Client config object for TCP client with server that inherits from TcpSshPropertiesConfig and adds properties for shared key and heartbeat
+ ///
+ public class TcpClientConfigObject
+ {
+ ///
+ /// TcpSsh Properties
+ ///
+ [JsonProperty("control")]
+ public ControlPropertiesConfig Control { get; set; }
+
+ ///
+ /// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic
+ ///
+ [JsonProperty("secure")]
+ public bool Secure { get; set; }
+
+ ///
+ /// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client
+ ///
+ [JsonProperty("sharedKeyRequired")]
+ public bool SharedKeyRequired { get; set; }
+
+ ///
+ /// The shared key that must match on the server and client
+ ///
+ [JsonProperty("sharedKey")]
+ public string SharedKey { get; set; }
+
+ ///
+ /// 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.
+ ///
+ [JsonProperty("heartbeatRequired")]
+ public bool HeartbeatRequired { get; set; }
+
+ ///
+ /// The interval in seconds for the heartbeat from the client. If not received client is disconnected
+ ///
+ [JsonProperty("heartbeatRequiredIntervalInSeconds")]
+ public ushort HeartbeatRequiredIntervalInSeconds { get; set; }
+
+ ///
+ /// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided.
+ ///
+ [JsonProperty("heartbeatStringToMatch")]
+ public string HeartbeatStringToMatch { get; set; }
+
+ ///
+ /// Receive Queue size must be greater than 20 or defaults to 20
+ ///
+ [JsonProperty("receiveQueueSize")]
+ public int ReceiveQueueSize { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/TcpServerConfigObject.cs b/src/PepperDash.Core/Comm/TcpServerConfigObject.cs
new file mode 100644
index 00000000..043cf58d
--- /dev/null
+++ b/src/PepperDash.Core/Comm/TcpServerConfigObject.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Tcp Server Config object with properties for a tcp server with shared key and heartbeat capabilities
+ ///
+ public class TcpServerConfigObject
+ {
+ ///
+ /// Uique key
+ ///
+ public string Key { get; set; }
+ ///
+ /// Max Clients that the server will allow to connect.
+ ///
+ public ushort MaxClients { get; set; }
+ ///
+ /// Bool value for secure. Currently not implemented in TCP sockets as they are not dynamic
+ ///
+ public bool Secure { get; set; }
+ ///
+ /// Port for the server to listen on
+ ///
+ public int Port { get; set; }
+ ///
+ /// Require a shared key that both server and client negotiate. If negotiation fails server disconnects the client
+ ///
+ public bool SharedKeyRequired { get; set; }
+ ///
+ /// The shared key that must match on the server and client
+ ///
+ public string SharedKey { get; set; }
+ ///
+ /// 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.
+ ///
+ public bool HeartbeatRequired { get; set; }
+ ///
+ /// The interval in seconds for the heartbeat from the client. If not received client is disconnected
+ ///
+ public ushort HeartbeatRequiredIntervalInSeconds { get; set; }
+ ///
+ /// HeartbeatString that will be checked against the message received. defaults to heartbeat if no string is provided.
+ ///
+ public string HeartbeatStringToMatch { get; set; }
+ ///
+ /// Client buffer size. See Crestron help. defaults to 2000 if not greater than 2000
+ ///
+ public int BufferSize { get; set; }
+ ///
+ /// Receive Queue size must be greater than 20 or defaults to 20
+ ///
+ public int ReceiveQueueSize { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Comm/eControlMethods.cs b/src/PepperDash.Core/Comm/eControlMethods.cs
new file mode 100644
index 00000000..28a95b12
--- /dev/null
+++ b/src/PepperDash.Core/Comm/eControlMethods.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Crestron Control Methods for a comm object
+ ///
+ public enum eControlMethod
+ {
+ ///
+ ///
+ ///
+ None = 0,
+ ///
+ /// RS232/422/485
+ ///
+ Com,
+ ///
+ /// Crestron IpId (most Crestron ethernet devices)
+ ///
+ IpId,
+ ///
+ /// Crestron IpIdTcp (HD-MD series, etc.)
+ ///
+ IpidTcp,
+ ///
+ /// Crestron IR control
+ ///
+ IR,
+ ///
+ /// SSH client
+ ///
+ Ssh,
+ ///
+ /// TCP/IP client
+ ///
+ Tcpip,
+ ///
+ /// Telnet
+ ///
+ Telnet,
+ ///
+ /// Crestnet device
+ ///
+ Cresnet,
+ ///
+ /// CEC Control, via a DM HDMI port
+ ///
+ Cec,
+ ///
+ /// UDP Server
+ ///
+ Udp,
+ ///
+ /// HTTP client
+ ///
+ Http,
+ ///
+ /// HTTPS client
+ ///
+ Https,
+ ///
+ /// Websocket client
+ ///
+ Ws,
+ ///
+ /// Secure Websocket client
+ ///
+ Wss,
+ ///
+ /// Secure TCP/IP
+ ///
+ SecureTcpIp
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/CommunicationExtras.cs b/src/PepperDash.Core/CommunicationExtras.cs
new file mode 100644
index 00000000..81fd76c5
--- /dev/null
+++ b/src/PepperDash.Core/CommunicationExtras.cs
@@ -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
+{
+ ///
+ /// An incoming communication stream
+ ///
+ public interface ICommunicationReceiver : IKeyed
+ {
+ ///
+ /// Notifies of bytes received
+ ///
+ event EventHandler BytesReceived;
+ ///
+ /// Notifies of text received
+ ///
+ event EventHandler TextReceived;
+
+ ///
+ /// Indicates connection status
+ ///
+ [JsonProperty("isConnected")]
+ bool IsConnected { get; }
+ ///
+ /// Connect to the device
+ ///
+ void Connect();
+ ///
+ /// Disconnect from the device
+ ///
+ void Disconnect();
+ }
+
+ ///
+ /// Represents a device that uses basic connection
+ ///
+ public interface IBasicCommunication : ICommunicationReceiver
+ {
+ ///
+ /// Send text to the device
+ ///
+ ///
+ void SendText(string text);
+
+ ///
+ /// Send bytes to the device
+ ///
+ ///
+ void SendBytes(byte[] bytes);
+ }
+
+ ///
+ /// Represents a device that implements IBasicCommunication and IStreamDebugging
+ ///
+ public interface IBasicCommunicationWithStreamDebugging : IBasicCommunication, IStreamDebugging
+ {
+
+ }
+
+ ///
+ /// Represents a device with stream debugging capablities
+ ///
+ public interface IStreamDebugging
+ {
+ ///
+ /// Object to enable stream debugging
+ ///
+ [JsonProperty("streamDebugging")]
+ CommunicationStreamDebugging StreamDebugging { get; }
+ }
+
+ ///
+ /// For IBasicCommunication classes that have SocketStatus. GenericSshClient,
+ /// GenericTcpIpClient
+ ///
+ public interface ISocketStatus : IBasicCommunication
+ {
+ ///
+ /// Notifies of socket status changes
+ ///
+ event EventHandler ConnectionChange;
+
+ ///
+ /// The current socket status of the client
+ ///
+ [JsonProperty("clientStatus")]
+ [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
+ SocketStatus ClientStatus { get; }
+ }
+
+ ///
+ /// Describes a device that implements ISocketStatus and IStreamDebugging
+ ///
+ public interface ISocketStatusWithStreamDebugging : ISocketStatus, IStreamDebugging
+ {
+
+ }
+
+ ///
+ /// Describes a device that can automatically attempt to reconnect
+ ///
+ public interface IAutoReconnect
+ {
+ ///
+ /// Enable automatic recconnect
+ ///
+ [JsonProperty("autoReconnect")]
+ bool AutoReconnect { get; set; }
+ ///
+ /// Interval in ms to attempt automatic recconnections
+ ///
+ [JsonProperty("autoReconnectIntervalMs")]
+ int AutoReconnectIntervalMs { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ public enum eGenericCommMethodStatusChangeType
+ {
+ ///
+ /// Connected
+ ///
+ Connected,
+ ///
+ /// Disconnected
+ ///
+ Disconnected
+ }
+
+ ///
+ /// This delegate defines handler for IBasicCommunication status changes
+ ///
+ /// Device firing the status change
+ ///
+ public delegate void GenericCommMethodStatusHandler(IBasicCommunication comm, eGenericCommMethodStatusChangeType status);
+
+ ///
+ ///
+ ///
+ public class GenericCommMethodReceiveBytesArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public byte[] Bytes { get; private set; }
+
+ ///
+ ///
+ ///
+ ///
+ public GenericCommMethodReceiveBytesArgs(byte[] bytes)
+ {
+ Bytes = bytes;
+ }
+
+ ///
+ /// S+ Constructor
+ ///
+ public GenericCommMethodReceiveBytesArgs() { }
+ }
+
+ ///
+ ///
+ ///
+ public class GenericCommMethodReceiveTextArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public string Text { get; private set; }
+ ///
+ ///
+ ///
+ public string Delimiter { get; private set; }
+ ///
+ ///
+ ///
+ ///
+ public GenericCommMethodReceiveTextArgs(string text)
+ {
+ Text = text;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public GenericCommMethodReceiveTextArgs(string text, string delimiter)
+ :this(text)
+ {
+ Delimiter = delimiter;
+ }
+
+ ///
+ /// S+ Constructor
+ ///
+ public GenericCommMethodReceiveTextArgs() { }
+ }
+
+
+
+ ///
+ ///
+ ///
+ public class ComTextHelper
+ {
+ ///
+ /// Gets escaped text for a byte array
+ ///
+ ///
+ ///
+ public static string GetEscapedText(byte[] bytes)
+ {
+ return String.Concat(bytes.Select(b => string.Format(@"[{0:X2}]", (int)b)).ToArray());
+ }
+
+ ///
+ /// Gets escaped text for a string
+ ///
+ ///
+ ///
+ 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());
+ }
+
+ ///
+ /// Gets debug text for a string
+ ///
+ ///
+ ///
+ public static string GetDebugText(string text)
+ {
+ return Regex.Replace(text, @"[^\u0020-\u007E]", a => GetEscapedText(a.Value));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Config/PortalConfigReader.cs b/src/PepperDash.Core/Config/PortalConfigReader.cs
new file mode 100644
index 00000000..43e9ea1e
--- /dev/null
+++ b/src/PepperDash.Core/Config/PortalConfigReader.cs
@@ -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
+{
+ ///
+ /// Reads a Portal formatted config file
+ ///
+ public class PortalConfigReader
+ {
+ ///
+ /// Reads the config file, checks if it needs a merge, merges and saves, then returns the merged Object.
+ ///
+ /// JObject of config file
+ 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();
+ }
+
+ if (jsonObj["template_url"] != null)
+ {
+ merged["templateUrl"] = jsonObj["template_url"].Value();
+ }
+
+ 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");
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ 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("uid") == tmplDev.Value("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;
+ }
+
+
+ ///
+ /// Helper for using with JTokens. Converts to JObject
+ ///
+ static JObject Merge(JToken t1, JToken t2, string path)
+ {
+ return Merge(JObject.FromObject(t1), JObject.FromObject(t2), path);
+ }
+
+ ///
+ /// Merge o2 onto o1
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Conversion/Convert.cs b/src/PepperDash.Core/Conversion/Convert.cs
new file mode 100644
index 00000000..2bafdcb0
--- /dev/null
+++ b/src/PepperDash.Core/Conversion/Convert.cs
@@ -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);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/CoreInterfaces.cs b/src/PepperDash.Core/CoreInterfaces.cs
new file mode 100644
index 00000000..6e0b639e
--- /dev/null
+++ b/src/PepperDash.Core/CoreInterfaces.cs
@@ -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
+{
+ ///
+ /// Unique key interface to require a unique key for the class
+ ///
+ public interface IKeyed
+ {
+ ///
+ /// Unique Key
+ ///
+ [JsonProperty("key")]
+ string Key { get; }
+ }
+
+ ///
+ /// Named Keyed device interface. Forces the device to have a Unique Key and a name.
+ ///
+ public interface IKeyName : IKeyed
+ {
+ ///
+ /// Isn't it obvious :)
+ ///
+ [JsonProperty("name")]
+ string Name { get; }
+ }
+
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Device.cs b/src/PepperDash.Core/Device.cs
new file mode 100644
index 00000000..fda30c4c
--- /dev/null
+++ b/src/PepperDash.Core/Device.cs
@@ -0,0 +1,179 @@
+using System;
+using System.Collections.Generic;
+using Serilog.Events;
+
+namespace PepperDash.Core
+{
+ //*********************************************************************************************************
+ ///
+ /// The core event and status-bearing class that most if not all device and connectors can derive from.
+ ///
+ public class Device : IKeyName
+ {
+
+ ///
+ /// Unique Key
+ ///
+ public string Key { get; protected set; }
+ ///
+ /// Name of the devie
+ ///
+ public string Name { get; protected set; }
+ ///
+ ///
+ ///
+ public bool Enabled { get; protected set; }
+
+ /////
+ ///// 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.
+ /////
+ //public DeviceConfig Config { get; private set; }
+ /////
+ ///// Helper method to check if Config exists
+ /////
+ //public bool HasConfig { get { return Config != null; } }
+
+ List _PreActivationActions;
+ List _PostActivationActions;
+
+ ///
+ ///
+ ///
+ public static Device DefaultDevice { get { return _DefaultDevice; } }
+ static Device _DefaultDevice = new Device("Default", "Default");
+
+ ///
+ /// Base constructor for all Devices.
+ ///
+ ///
+ public Device(string key)
+ {
+ Key = key;
+ if (key.Contains(".")) Debug.LogMessage(LogEventLevel.Information, "WARNING: Device key should not include '.'", this);
+ Name = "";
+ }
+
+ ///
+ /// Constructor with key and name
+ ///
+ ///
+ ///
+ public Device(string key, string name) : this(key)
+ {
+ Name = name;
+
+ }
+
+ //public Device(DeviceConfig config)
+ // : this(config.Key, config.Name)
+ //{
+ // Config = config;
+ //}
+
+ ///
+ /// Adds a pre activation action
+ ///
+ ///
+ public void AddPreActivationAction(Action act)
+ {
+ if (_PreActivationActions == null)
+ _PreActivationActions = new List();
+ _PreActivationActions.Add(act);
+ }
+
+ ///
+ /// Adds a post activation action
+ ///
+ ///
+ public void AddPostActivationAction(Action act)
+ {
+ if (_PostActivationActions == null)
+ _PostActivationActions = new List();
+ _PostActivationActions.Add(act);
+ }
+
+ ///
+ /// Executes the preactivation actions
+ ///
+ public void PreActivate()
+ {
+ if (_PreActivationActions != null)
+ _PreActivationActions.ForEach(a => {
+ try
+ {
+ a.Invoke();
+ } catch (Exception e)
+ {
+ Debug.LogMessage(e, "Error in PreActivationAction: " + e.Message, this);
+ }
+ });
+ }
+
+ ///
+ /// 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()
+ ///
+ 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;
+ }
+
+ ///
+ /// Executes the postactivation actions
+ ///
+ public void PostActivate()
+ {
+ if (_PostActivationActions != null)
+ _PostActivationActions.ForEach(a => {
+ try
+ {
+ a.Invoke();
+ }
+ catch (Exception e)
+ {
+ Debug.LogMessage(e, "Error in PostActivationAction: " + e.Message, this);
+ }
+ });
+ }
+
+ ///
+ /// 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()
+ ///
+ /// true if device activated successfully.
+ public virtual bool CustomActivate() { return true; }
+
+ ///
+ /// Call to deactivate device - unlink events, etc. Overriding classes do not
+ /// need to call base.Deactivate()
+ ///
+ ///
+ public virtual bool Deactivate() { return true; }
+
+ ///
+ /// Call this method to start communications with a device. Overriding classes do not need to call base.Initialize()
+ ///
+ public virtual void Initialize()
+ {
+ }
+
+ ///
+ /// Helper method to check object for bool value false and fire an Action method
+ ///
+ /// Should be of type bool, others will be ignored
+ /// Action to be run when o is false
+ public void OnFalse(object o, Action a)
+ {
+ if (o is bool && !(bool)o) a();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Directory.build.targets b/src/PepperDash.Core/Directory.build.targets
new file mode 100644
index 00000000..0ef185c7
--- /dev/null
+++ b/src/PepperDash.Core/Directory.build.targets
@@ -0,0 +1,43 @@
+
+
+
+ true
+ content;
+
+
+ true
+ content;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ doNotUse
+
+
+
+
\ No newline at end of file
diff --git a/src/PepperDash.Core/EssentialsPlugins-builds-4-series-caller.yml b/src/PepperDash.Core/EssentialsPlugins-builds-4-series-caller.yml
new file mode 100644
index 00000000..c4b4d2c4
--- /dev/null
+++ b/src/PepperDash.Core/EssentialsPlugins-builds-4-series-caller.yml
@@ -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 }}
\ No newline at end of file
diff --git a/src/PepperDash.Core/EthernetHelper.cs b/src/PepperDash.Core/EthernetHelper.cs
new file mode 100644
index 00000000..88429886
--- /dev/null
+++ b/src/PepperDash.Core/EthernetHelper.cs
@@ -0,0 +1,117 @@
+using Crestron.SimplSharp;
+using Newtonsoft.Json;
+using Serilog.Events;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Class to help with accessing values from the CrestronEthernetHelper class
+ ///
+ public class EthernetHelper
+ {
+ ///
+ ///
+ ///
+ public static EthernetHelper LanHelper
+ {
+ get
+ {
+ if (_LanHelper == null) _LanHelper = new EthernetHelper(0);
+ return _LanHelper;
+ }
+ }
+ static EthernetHelper _LanHelper;
+
+ // ADD OTHER HELPERS HERE
+
+ ///
+ ///
+ ///
+ public int PortNumber { get; private set; }
+
+ private EthernetHelper(int portNumber)
+ {
+ PortNumber = portNumber;
+ }
+
+ ///
+ ///
+ ///
+ [JsonProperty("linkActive")]
+ public bool LinkActive
+ {
+ get
+ {
+ var status = CrestronEthernetHelper.GetEthernetParameter(
+ CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_LINK_STATUS, 0);
+ Debug.LogMessage(LogEventLevel.Information, "LinkActive = {0}", status);
+ return status == "";
+ }
+ }
+
+ ///
+ ///
+ ///
+ [JsonProperty("dchpActive")]
+ public bool DhcpActive
+ {
+ get
+ {
+ return CrestronEthernetHelper.GetEthernetParameter(
+ CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_DHCP_STATE, 0) == "ON";
+ }
+ }
+
+ ///
+ ///
+ ///
+ [JsonProperty("hostname")]
+ public string Hostname
+ {
+ get
+ {
+ return CrestronEthernetHelper.GetEthernetParameter(
+ CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_HOSTNAME, 0);
+ }
+ }
+
+ ///
+ ///
+ ///
+ [JsonProperty("ipAddress")]
+ public string IPAddress
+ {
+ get
+ {
+ return CrestronEthernetHelper.GetEthernetParameter(
+ CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
+ }
+ }
+
+ ///
+ ///
+ ///
+ [JsonProperty("subnetMask")]
+ public string SubnetMask
+ {
+ get
+ {
+ return CrestronEthernetHelper.GetEthernetParameter(
+ CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_MASK, 0);
+ }
+ }
+
+ ///
+ ///
+ ///
+ [JsonProperty("defaultGateway")]
+ public string DefaultGateway
+ {
+ get
+ {
+ return CrestronEthernetHelper.GetEthernetParameter(
+ CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_ROUTER, 0);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/EventArgs.cs b/src/PepperDash.Core/EventArgs.cs
new file mode 100644
index 00000000..29ef13a8
--- /dev/null
+++ b/src/PepperDash.Core/EventArgs.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Bool change event args
+ ///
+ public class BoolChangeEventArgs : EventArgs
+ {
+ ///
+ /// Boolean state property
+ ///
+ public bool State { get; set; }
+
+ ///
+ /// Boolean ushort value property
+ ///
+ public ushort IntValue { get { return (ushort)(State ? 1 : 0); } }
+
+ ///
+ /// Boolean change event args type
+ ///
+ public ushort Type { get; set; }
+
+ ///
+ /// Boolean change event args index
+ ///
+ public ushort Index { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public BoolChangeEventArgs()
+ {
+
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ public BoolChangeEventArgs(bool state, ushort type)
+ {
+ State = state;
+ Type = type;
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ ///
+ public BoolChangeEventArgs(bool state, ushort type, ushort index)
+ {
+ State = state;
+ Type = type;
+ Index = index;
+ }
+ }
+
+ ///
+ /// Ushort change event args
+ ///
+ public class UshrtChangeEventArgs : EventArgs
+ {
+ ///
+ /// Ushort change event args integer value
+ ///
+ public ushort IntValue { get; set; }
+
+ ///
+ /// Ushort change event args type
+ ///
+ public ushort Type { get; set; }
+
+ ///
+ /// Ushort change event args index
+ ///
+ public ushort Index { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public UshrtChangeEventArgs()
+ {
+
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ public UshrtChangeEventArgs(ushort intValue, ushort type)
+ {
+ IntValue = intValue;
+ Type = type;
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ ///
+ public UshrtChangeEventArgs(ushort intValue, ushort type, ushort index)
+ {
+ IntValue = intValue;
+ Type = type;
+ Index = index;
+ }
+ }
+
+ ///
+ /// String change event args
+ ///
+ public class StringChangeEventArgs : EventArgs
+ {
+ ///
+ /// String change event args value
+ ///
+ public string StringValue { get; set; }
+
+ ///
+ /// String change event args type
+ ///
+ public ushort Type { get; set; }
+
+ ///
+ /// string change event args index
+ ///
+ public ushort Index { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public StringChangeEventArgs()
+ {
+
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ public StringChangeEventArgs(string stringValue, ushort type)
+ {
+ StringValue = stringValue;
+ Type = type;
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ ///
+ public StringChangeEventArgs(string stringValue, ushort type, ushort index)
+ {
+ StringValue = stringValue;
+ Type = type;
+ Index = index;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs b/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs
new file mode 100644
index 00000000..1b78c33f
--- /dev/null
+++ b/src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs
@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.GenericRESTfulCommunications
+{
+ ///
+ /// Constants
+ ///
+ public class GenericRESTfulConstants
+ {
+ ///
+ /// Generic boolean change
+ ///
+ public const ushort BoolValueChange = 1;
+ ///
+ /// Generic Ushort change
+ ///
+ public const ushort UshrtValueChange = 101;
+ ///
+ /// Response Code Ushort change
+ ///
+ public const ushort ResponseCodeChange = 102;
+ ///
+ /// Generic String chagne
+ ///
+ public const ushort StringValueChange = 201;
+ ///
+ /// Response string change
+ ///
+ public const ushort ResponseStringChange = 202;
+ ///
+ /// Error string change
+ ///
+ public const ushort ErrorStringChange = 203;
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs b/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs
new file mode 100644
index 00000000..bd33e13c
--- /dev/null
+++ b/src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs
@@ -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
+{
+ ///
+ /// Generic RESTful communication class
+ ///
+ public class GenericRESTfulClient
+ {
+ ///
+ /// Boolean event handler
+ ///
+ public event EventHandler BoolChange;
+ ///
+ /// Ushort event handler
+ ///
+ public event EventHandler UshrtChange;
+ ///
+ /// String event handler
+ ///
+ public event EventHandler StringChange;
+
+ ///
+ /// Constructor
+ ///
+ public GenericRESTfulClient()
+ {
+
+ }
+
+ ///
+ /// Generic RESTful submit request
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Private HTTP submit request
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Private HTTPS submit request
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Private method to encode username and password to Base64 string
+ ///
+ ///
+ ///
+ /// authorization
+ 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;
+ }
+
+ ///
+ /// Protected method to handle boolean change events
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Protected mehtod to handle ushort change events
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Protected method to handle string change events
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonStandardObjects/EventArgs and Constants.cs b/src/PepperDash.Core/JsonStandardObjects/EventArgs and Constants.cs
new file mode 100644
index 00000000..ed02ccb0
--- /dev/null
+++ b/src/PepperDash.Core/JsonStandardObjects/EventArgs and Constants.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.JsonStandardObjects
+{
+ ///
+ /// Constants for simpl modules
+ ///
+ public class JsonStandardDeviceConstants
+ {
+ ///
+ /// Json object evaluated constant
+ ///
+ public const ushort JsonObjectEvaluated = 2;
+
+ ///
+ /// Json object changed constant
+ ///
+ public const ushort JsonObjectChanged = 104;
+ }
+
+ ///
+ ///
+ ///
+ public class DeviceChangeEventArgs : EventArgs
+ {
+ ///
+ /// Device change event args object
+ ///
+ public DeviceConfig Device { get; set; }
+
+ ///
+ /// Device change event args type
+ ///
+ public ushort Type { get; set; }
+
+ ///
+ /// Device change event args index
+ ///
+ public ushort Index { get; set; }
+
+ ///
+ /// Default constructor
+ ///
+ public DeviceChangeEventArgs()
+ {
+
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ public DeviceChangeEventArgs(DeviceConfig device, ushort type)
+ {
+ Device = device;
+ Type = type;
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ ///
+ public DeviceChangeEventArgs(DeviceConfig device, ushort type, ushort index)
+ {
+ Device = device;
+ Type = type;
+ Index = index;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDevice.cs b/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDevice.cs
new file mode 100644
index 00000000..3abfd36a
--- /dev/null
+++ b/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDevice.cs
@@ -0,0 +1,183 @@
+using System;
+using System.Linq;
+using Crestron.SimplSharp;
+using PepperDash.Core.JsonToSimpl;
+using Serilog.Events;
+
+namespace PepperDash.Core.JsonStandardObjects
+{
+ ///
+ /// Device class
+ ///
+ public class DeviceConfig
+ {
+ ///
+ /// JSON config key property
+ ///
+ public string key { get; set; }
+ ///
+ /// JSON config name property
+ ///
+ public string name { get; set; }
+ ///
+ /// JSON config type property
+ ///
+ public string type { get; set; }
+ ///
+ /// JSON config properties
+ ///
+ public PropertiesConfig properties { get; set; }
+
+ ///
+ /// Bool change event handler
+ ///
+ public event EventHandler BoolChange;
+ ///
+ /// Ushort change event handler
+ ///
+ public event EventHandler UshrtChange;
+ ///
+ /// String change event handler
+ ///
+ public event EventHandler StringChange;
+ ///
+ /// Object change event handler
+ ///
+ public event EventHandler DeviceChange;
+
+ ///
+ /// Constructor
+ ///
+ public DeviceConfig()
+ {
+ properties = new PropertiesConfig();
+ }
+
+ ///
+ /// Initialize method
+ ///
+ ///
+ ///
+ 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().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
+
+ ///
+ /// BoolChange event handler helper
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// UshrtChange event handler helper
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// StringChange event handler helper
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// ObjectChange event handler helper
+ ///
+ ///
+ ///
+ ///
+ 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
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDeviceConfig.cs b/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDeviceConfig.cs
new file mode 100644
index 00000000..fa23d87e
--- /dev/null
+++ b/src/PepperDash.Core/JsonStandardObjects/JsonToSimplDeviceConfig.cs
@@ -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
+ }
+ }
+ }
+ }
+ ]
+ }
+ */
+ ///
+ /// Device communication parameter class
+ ///
+ public class ComParamsConfig
+ {
+ ///
+ ///
+ ///
+ public int baudRate { get; set; }
+ ///
+ ///
+ ///
+ public int dataBits { get; set; }
+ ///
+ ///
+ ///
+ public int stopBits { get; set; }
+ ///
+ ///
+ ///
+ public string parity { get; set; }
+ ///
+ ///
+ ///
+ public string protocol { get; set; }
+ ///
+ ///
+ ///
+ public string hardwareHandshake { get; set; }
+ ///
+ ///
+ ///
+ public string softwareHandshake { get; set; }
+ ///
+ ///
+ ///
+ public int pacing { get; set; }
+
+ // convert properties for simpl
+ ///
+ ///
+ ///
+ public ushort simplBaudRate { get { return Convert.ToUInt16(baudRate); } }
+ ///
+ ///
+ ///
+ public ushort simplDataBits { get { return Convert.ToUInt16(dataBits); } }
+ ///
+ ///
+ ///
+ public ushort simplStopBits { get { return Convert.ToUInt16(stopBits); } }
+ ///
+ ///
+ ///
+ public ushort simplPacing { get { return Convert.ToUInt16(pacing); } }
+
+ ///
+ /// Constructor
+ ///
+ public ComParamsConfig()
+ {
+
+ }
+ }
+
+ ///
+ /// Device TCP/SSH properties class
+ ///
+ public class TcpSshPropertiesConfig
+ {
+ ///
+ ///
+ ///
+ public string address { get; set; }
+ ///
+ ///
+ ///
+ public int port { get; set; }
+ ///
+ ///
+ ///
+ public string username { get; set; }
+ ///
+ ///
+ ///
+ public string password { get; set; }
+ ///
+ ///
+ ///
+ public bool autoReconnect { get; set; }
+ ///
+ ///
+ ///
+ public int autoReconnectIntervalMs { get; set; }
+
+ // convert properties for simpl
+ ///
+ ///
+ ///
+ public ushort simplPort { get { return Convert.ToUInt16(port); } }
+ ///
+ ///
+ ///
+ public ushort simplAutoReconnect { get { return (ushort)(autoReconnect ? 1 : 0); } }
+ ///
+ ///
+ ///
+ public ushort simplAutoReconnectIntervalMs { get { return Convert.ToUInt16(autoReconnectIntervalMs); } }
+
+ ///
+ /// Constructor
+ ///
+ public TcpSshPropertiesConfig()
+ {
+
+ }
+ }
+
+ ///
+ /// Device control class
+ ///
+ public class ControlConfig
+ {
+ ///
+ ///
+ ///
+ public string method { get; set; }
+ ///
+ ///
+ ///
+ public string controlPortDevKey { get; set; }
+ ///
+ ///
+ ///
+ public int controlPortNumber { get; set; }
+ ///
+ ///
+ ///
+ public ComParamsConfig comParams { get; set; }
+ ///
+ ///
+ ///
+ public TcpSshPropertiesConfig tcpSshProperties { get; set; }
+
+ // convert properties for simpl
+ ///
+ ///
+ ///
+ public ushort simplControlPortNumber { get { return Convert.ToUInt16(controlPortNumber); } }
+
+ ///
+ /// Constructor
+ ///
+ public ControlConfig()
+ {
+ comParams = new ComParamsConfig();
+ tcpSshProperties = new TcpSshPropertiesConfig();
+ }
+ }
+
+ ///
+ /// Device properties class
+ ///
+ public class PropertiesConfig
+ {
+ ///
+ ///
+ ///
+ public int deviceId { get; set; }
+ ///
+ ///
+ ///
+ public bool enabled { get; set; }
+ ///
+ ///
+ ///
+ public ControlConfig control { get; set; }
+
+ // convert properties for simpl
+ ///
+ ///
+ ///
+ public ushort simplDeviceId { get { return Convert.ToUInt16(deviceId); } }
+ ///
+ ///
+ ///
+ public ushort simplEnabled { get { return (ushort)(enabled ? 1 : 0); } }
+
+ ///
+ /// Constructor
+ ///
+ public PropertiesConfig()
+ {
+ control = new ControlConfig();
+ }
+ }
+
+ ///
+ /// Root device class
+ ///
+ public class RootObject
+ {
+ ///
+ /// The collection of devices
+ ///
+ public List devices { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonToSimpl/Constants.cs b/src/PepperDash.Core/JsonToSimpl/Constants.cs
new file mode 100644
index 00000000..d87b50c2
--- /dev/null
+++ b/src/PepperDash.Core/JsonToSimpl/Constants.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.JsonToSimpl
+{
+ ///
+ /// Constants for Simpl modules
+ ///
+ public class JsonToSimplConstants
+ {
+ ///
+ ///
+ ///
+ public const ushort BoolValueChange = 1;
+ ///
+ ///
+ ///
+ public const ushort JsonIsValidBoolChange = 2;
+
+ ///
+ /// Reports the if the device is 3-series compatible
+ ///
+ public const ushort ProgramCompatibility3SeriesChange = 3;
+
+ ///
+ /// Reports the if the device is 4-series compatible
+ ///
+ public const ushort ProgramCompatibility4SeriesChange = 4;
+
+ ///
+ /// Reports the device platform enum value
+ ///
+ public const ushort DevicePlatformValueChange = 5;
+
+ ///
+ ///
+ ///
+ public const ushort UshortValueChange = 101;
+
+ ///
+ ///
+ ///
+ public const ushort StringValueChange = 201;
+ ///
+ ///
+ ///
+ public const ushort FullPathToArrayChange = 202;
+ ///
+ ///
+ ///
+ public const ushort ActualFilePathChange = 203;
+
+ ///
+ ///
+ ///
+ public const ushort FilenameResolvedChange = 204;
+ ///
+ ///
+ ///
+ public const ushort FilePathResolvedChange = 205;
+
+ ///
+ /// Reports the root directory change
+ ///
+ public const ushort RootDirectoryChange = 206;
+
+ ///
+ /// Reports the room ID change
+ ///
+ public const ushort RoomIdChange = 207;
+
+ ///
+ /// Reports the room name change
+ ///
+ public const ushort RoomNameChange = 208;
+ }
+
+ ///
+ /// S+ values delegate
+ ///
+ public delegate void SPlusValuesDelegate();
+
+ ///
+ /// S+ values wrapper
+ ///
+ public class SPlusValueWrapper
+ {
+ ///
+ ///
+ ///
+ public SPlusType ValueType { get; private set; }
+ ///
+ ///
+ ///
+ public ushort Index { get; private set; }
+ ///
+ ///
+ ///
+ public ushort BoolUShortValue { get; set; }
+ ///
+ ///
+ ///
+ public string StringValue { get; set; }
+
+ ///
+ ///
+ ///
+ public SPlusValueWrapper() {}
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public SPlusValueWrapper(SPlusType type, ushort index)
+ {
+ ValueType = type;
+ Index = index;
+ }
+ }
+
+ ///
+ /// S+ types enum
+ ///
+ public enum SPlusType
+ {
+ ///
+ /// Digital
+ ///
+ Digital,
+ ///
+ /// Analog
+ ///
+ Analog,
+ ///
+ /// String
+ ///
+ String
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonToSimpl/Global.cs b/src/PepperDash.Core/JsonToSimpl/Global.cs
new file mode 100644
index 00000000..8392fa61
--- /dev/null
+++ b/src/PepperDash.Core/JsonToSimpl/Global.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+using Serilog.Events;
+
+//using PepperDash.Core;
+
+namespace PepperDash.Core.JsonToSimpl
+{
+ ///
+ /// The global class to manage all the instances of JsonToSimplMaster
+ ///
+ public class J2SGlobal
+ {
+ static List Masters = new List();
+
+
+ ///
+ /// Adds a file master. If the master's key or filename is equivalent to any existing
+ /// master, this will fail
+ ///
+ /// New master to add
+ ///
+ 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.LogMessage(LogEventLevel.Debug, "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);
+ }
+ }
+
+ ///
+ /// Gets a master by its key. Case-insensitive
+ ///
+ public static JsonToSimplMaster GetMasterByFile(string file)
+ {
+ return Masters.FirstOrDefault(m => m.UniqueID.Equals(file, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs
new file mode 100644
index 00000000..c94dad29
--- /dev/null
+++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs
@@ -0,0 +1,162 @@
+using System;
+using System.Linq;
+using Newtonsoft.Json.Linq;
+using Serilog.Events;
+
+namespace PepperDash.Core.JsonToSimpl
+{
+ ///
+ /// Used to interact with an array of values with the S+ modules
+ ///
+ public class JsonToSimplArrayLookupChild : JsonToSimplChildObjectBase
+ {
+ ///
+ ///
+ ///
+ public string SearchPropertyName { get; set; }
+ ///
+ ///
+ ///
+ public string SearchPropertyValue { get; set; }
+
+ int ArrayIndex;
+
+ ///
+ /// For gt2.4.1 array lookups
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+
+ ///
+ /// For newer >=2.4.1 array lookups.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected override string GetFullPath(string path)
+ {
+ return string.Format("{0}[{1}].{2}{3}",
+ PathPrefix == null ? "" : PathPrefix,
+ ArrayIndex,
+ path,
+ PathSuffix == null ? "" : PathSuffix);
+ }
+
+ ///
+ /// Process all values
+ ///
+ public override void ProcessAll()
+ {
+ if (FindInArray())
+ base.ProcessAll();
+ }
+
+ ///
+ /// Provides the path append for GetFullPath
+ ///
+ ///
+ string GetPathAppend(string a)
+ {
+ if (string.IsNullOrEmpty(a))
+ {
+ return "";
+ }
+ if (a.StartsWith("."))
+ {
+ return a;
+ }
+ else
+ {
+ return "." + a;
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ 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()
+ .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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs
new file mode 100644
index 00000000..5aa67c96
--- /dev/null
+++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs
@@ -0,0 +1,404 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Newtonsoft.Json.Linq;
+
+namespace PepperDash.Core.JsonToSimpl
+{
+ ///
+ /// Base class for JSON objects
+ ///
+ public abstract class JsonToSimplChildObjectBase : IKeyed
+ {
+ ///
+ /// Notifies of bool change
+ ///
+ public event EventHandler BoolChange;
+ ///
+ /// Notifies of ushort change
+ ///
+ public event EventHandler UShortChange;
+ ///
+ /// Notifies of string change
+ ///
+ public event EventHandler StringChange;
+
+ ///
+ /// Delegate to get all values
+ ///
+ public SPlusValuesDelegate GetAllValuesDelegate { get; set; }
+
+ ///
+ /// Use a callback to reduce task switch/threading
+ ///
+ public SPlusValuesDelegate SetAllPathsDelegate { get; set; }
+
+ ///
+ /// Unique identifier for instance
+ ///
+ public string Key { get; protected set; }
+
+ ///
+ /// This will be prepended to all paths to allow path swapping or for more organized
+ /// sub-paths
+ ///
+ public string PathPrefix { get; protected set; }
+
+ ///
+ /// This is added to the end of all paths
+ ///
+ public string PathSuffix { get; protected set; }
+
+ ///
+ /// Indicates if the instance is linked to an object
+ ///
+ public bool LinkedToObject { get; protected set; }
+
+ ///
+ /// Reference to Master instance
+ ///
+ protected JsonToSimplMaster Master;
+
+ ///
+ /// Paths to boolean values in JSON structure
+ ///
+ protected Dictionary BoolPaths = new Dictionary();
+ ///
+ /// Paths to numeric values in JSON structure
+ ///
+ protected Dictionary UshortPaths = new Dictionary();
+ ///
+ /// Paths to string values in JSON structure
+ ///
+ protected Dictionary StringPaths = new Dictionary();
+
+ ///
+ /// Call this before doing anything else
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Sets the path prefix for the object
+ ///
+ ///
+ public void SetPathPrefix(string pathPrefix)
+ {
+ PathPrefix = pathPrefix;
+ }
+ ///
+ /// Set the JPath to evaluate for a given bool out index.
+ ///
+ 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;
+ }
+
+ ///
+ /// Set the JPath for a ushort out index.
+ ///
+ 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;
+ }
+
+ ///
+ /// Set the JPath for a string output index.
+ ///
+ 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;
+ }
+
+ ///
+ /// Evalutates all outputs with defined paths. called by S+ when paths are ready to process
+ /// and by Master when file is read.
+ ///
+ 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);
+ }
+
+ ///
+ /// Processes a bool property, converting to bool, firing off a BoolChange event
+ ///
+ 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);
+ }
+
+ ///
+ /// Processes the given path.
+ ///
+ /// JPath formatted path to the desired property
+ /// The string value of the property, or a default value if it
+ /// doesn't exist
+ /// This will return false in the case that EvaulateAllOnJsonChange
+ /// is false and the path does not evaluate to a property in the incoming JSON.
+ 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();
+ 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
+
+
+ ///
+ /// Called from Master to read inputs and update their values in master JObject
+ /// Callback should hit one of the following four methods
+ ///
+ 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();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void USetBoolValue(ushort key, ushort theValue)
+ {
+ SetBoolValue(key, theValue == 1);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void SetBoolValue(ushort key, bool theValue)
+ {
+ if (BoolPaths.ContainsKey(key))
+ SetValueOnMaster(BoolPaths[key], new JValue(theValue));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void SetUShortValue(ushort key, ushort theValue)
+ {
+ if (UshortPaths.ContainsKey(key))
+ SetValueOnMaster(UshortPaths[key], new JValue(theValue));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void SetStringValue(ushort key, string theValue)
+ {
+ if (StringPaths.ContainsKey(key))
+ SetValueOnMaster(StringPaths[key], new JValue(theValue));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ protected virtual string GetFullPath(string path)
+ {
+ return (PathPrefix != null ? PathPrefix : "") +
+ path + (PathSuffix != null ? PathSuffix : "");
+ }
+
+ // Helpers for events
+ //******************************************************************************************
+ ///
+ /// Event helper
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ //******************************************************************************************
+ ///
+ /// Event helper
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Event helper
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs
new file mode 100644
index 00000000..411fbdc5
--- /dev/null
+++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs
@@ -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
+{
+ ///
+ /// Represents a JSON file that can be read and written to
+ ///
+ public class JsonToSimplFileMaster : JsonToSimplMaster
+ {
+ ///
+ /// Sets the filepath as well as registers this with the Global.Masters list
+ ///
+ public string Filepath { get; private set; }
+
+ ///
+ /// Filepath to the actual file that will be read (Portal or local)
+ ///
+ public string ActualFilePath { get; private set; }
+
+ ///
+ ///
+ ///
+ public string Filename { get; private set; }
+ ///
+ ///
+ ///
+ 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();
+
+ /*****************************************************************************************/
+
+ ///
+ /// SIMPL+ default constructor.
+ ///
+ public JsonToSimplFileMaster()
+ {
+ }
+
+ ///
+ /// Read, evaluate and udpate status
+ ///
+ 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);
+ }
+ }
+
+
+ ///
+ /// Sets the debug level
+ ///
+ ///
+ public void setDebugLevel(uint level)
+ {
+ Debug.SetDebugLevel(level);
+ }
+
+ ///
+ /// Saves the values to the file
+ ///
+ public override void Save()
+ {
+ // this code is duplicated in the other masters!!!!!!!!!!!!!
+ UnsavedValues = new Dictionary();
+ // 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;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplFixedPathObject.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFixedPathObject.cs
new file mode 100644
index 00000000..3e69ed9d
--- /dev/null
+++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplFixedPathObject.cs
@@ -0,0 +1,18 @@
+
+
+namespace PepperDash.Core.JsonToSimpl
+{
+ ///
+ ///
+ ///
+ public class JsonToSimplFixedPathObject : JsonToSimplChildObjectBase
+ {
+ ///
+ /// Constructor
+ ///
+ public JsonToSimplFixedPathObject()
+ {
+ this.LinkedToObject = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs
new file mode 100644
index 00000000..e0f42f8e
--- /dev/null
+++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs
@@ -0,0 +1,118 @@
+using System;
+using System.Collections.Generic;
+using Crestron.SimplSharp;
+using Newtonsoft.Json.Linq;
+
+namespace PepperDash.Core.JsonToSimpl
+{
+ ///
+ /// Generic Master
+ ///
+ 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();
+
+ ///
+ /// Callback action for saving
+ ///
+ public Action SaveCallback { get; set; }
+
+ /*****************************************************************************************/
+
+ ///
+ /// SIMPL+ default constructor.
+ ///
+ public JsonToSimplGenericMaster()
+ {
+ }
+
+ ///
+ /// Loads in JSON and triggers evaluation on all children
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Loads JSON into JsonObject, but does not trigger evaluation by children
+ ///
+ ///
+ public void SetJsonWithoutEvaluating(string json)
+ {
+ try
+ {
+ JsonObject = JObject.Parse(json);
+ }
+ catch (Exception e)
+ {
+ Debug.Console(0, this, "JSON parsing failed:\r{0}", e);
+ }
+ }
+
+ ///
+ ///
+ ///
+ public override void Save()
+ {
+ // this code is duplicated in the other masters!!!!!!!!!!!!!
+ UnsavedValues = new Dictionary();
+ // 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.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs
new file mode 100644
index 00000000..2f872e41
--- /dev/null
+++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs
@@ -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
+{
+ ///
+ /// Abstract base class for JsonToSimpl interactions
+ ///
+ public abstract class JsonToSimplMaster : IKeyed
+ {
+ ///
+ /// Notifies of bool change
+ ///
+ public event EventHandler BoolChange;
+ ///
+ /// Notifies of ushort change
+ ///
+ public event EventHandler UshrtChange;
+ ///
+ /// Notifies of string change
+ ///
+ public event EventHandler StringChange;
+
+ ///
+ /// A collection of associated child modules
+ ///
+ protected List Children = new List();
+
+ /*****************************************************************************************/
+
+ ///
+ /// Mirrors the Unique ID for now.
+ ///
+ public string Key { get { return UniqueID; } }
+
+ ///
+ /// A unique ID
+ ///
+ public string UniqueID { get; protected set; }
+
+ ///
+ /// Merely for use in debug messages
+ ///
+ public string DebugName
+ {
+ get { return _DebugName; }
+ set { if (DebugName == null) _DebugName = ""; else _DebugName = value; }
+ }
+ string _DebugName = "";
+
+ ///
+ /// This will be prepended to all paths to allow path swapping or for more organized
+ /// sub-paths
+ ///
+ public string PathPrefix { get; set; }
+
+ ///
+ /// This is added to the end of all paths
+ ///
+ public string PathSuffix { get; set; }
+
+ ///
+ /// Enables debugging output to the console. Certain error messages will be logged to the
+ /// system's error log regardless of this setting
+ ///
+ public bool DebugOn { get; set; }
+
+ ///
+ /// Ushort helper for Debug property
+ ///
+ public ushort UDebug
+ {
+ get { return (ushort)(DebugOn ? 1 : 0); }
+ set
+ {
+ DebugOn = (value == 1);
+ CrestronConsole.PrintLine("JsonToSimpl debug={0}", DebugOn);
+ }
+ }
+
+ ///
+ ///
+ ///
+ public JObject JsonObject { get; protected set; }
+
+ /*****************************************************************************************/
+ /** Privates **/
+
+
+ // The JSON file in JObject form
+ // For gathering the incoming data
+ protected Dictionary UnsavedValues = new Dictionary();
+
+ /*****************************************************************************************/
+
+ ///
+ /// SIMPL+ default constructor.
+ ///
+ public JsonToSimplMaster()
+ {
+ }
+
+
+ ///
+ /// Sets up class - overriding methods should always call this.
+ ///
+ ///
+ public virtual void Initialize(string uniqueId)
+ {
+ UniqueID = uniqueId;
+ J2SGlobal.AddMaster(this); // Should not re-add
+ }
+
+ ///
+ /// Adds a child "module" to this master
+ ///
+ ///
+ public void AddChild(JsonToSimplChildObjectBase child)
+ {
+ if (!Children.Contains(child))
+ {
+ Children.Add(child);
+ }
+ }
+
+ ///
+ /// Called from the child to add changed or new values for saving
+ ///
+ 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);
+ }
+
+ ///
+ /// Saves the file
+ ///
+ public abstract void Save();
+
+
+ ///
+ ///
+ ///
+ public static class JsonFixes
+ {
+ ///
+ /// Deserializes a string into a JObject
+ ///
+ ///
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Deserializes a string into a JArray
+ ///
+ ///
+ ///
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// Helper event
+ ///
+ ///
+ ///
+ ///
+ protected void OnBoolChange(bool state, ushort index, ushort type)
+ {
+ if (BoolChange != null)
+ {
+ var args = new BoolChangeEventArgs(state, type);
+ args.Index = index;
+ BoolChange(this, args);
+ }
+ }
+
+ ///
+ /// Helper event
+ ///
+ ///
+ ///
+ ///
+ protected void OnUshrtChange(ushort state, ushort index, ushort type)
+ {
+ if (UshrtChange != null)
+ {
+ var args = new UshrtChangeEventArgs(state, type);
+ args.Index = index;
+ UshrtChange(this, args);
+ }
+ }
+
+ ///
+ /// Helper event
+ ///
+ ///
+ ///
+ ///
+ protected void OnStringChange(string value, ushort index, ushort type)
+ {
+ if (StringChange != null)
+ {
+ var args = new StringChangeEventArgs(value, type);
+ args.Index = index;
+ StringChange(this, args);
+ }
+ }
+ }
+}
diff --git a/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs b/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs
new file mode 100644
index 00000000..c170a9a1
--- /dev/null
+++ b/src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs
@@ -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
+{
+ ///
+ /// Portal File Master
+ ///
+ public class JsonToSimplPortalFileMaster : JsonToSimplMaster
+ {
+ ///
+ /// Sets the filepath as well as registers this with the Global.Masters list
+ ///
+ public string PortalFilepath { get; private set; }
+
+ ///
+ /// File path of the actual file being read (Portal or local)
+ ///
+ public string ActualFilePath { get; private set; }
+
+ /*****************************************************************************************/
+ /** Privates **/
+
+ // To prevent multiple same-file access
+ object StringBuilderLock = new object();
+ static object FileLock = new object();
+
+ /*****************************************************************************************/
+
+ ///
+ /// SIMPL+ default constructor.
+ ///
+ public JsonToSimplPortalFileMaster()
+ {
+ }
+
+ ///
+ /// Read, evaluate and udpate status
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Returns the FileInfo object for a given path, with possible wildcards
+ ///
+ ///
+ ///
+ 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();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public void setDebugLevel(uint level)
+ {
+ Debug.SetDebugLevel(level);
+ }
+
+ ///
+ ///
+ ///
+ public override void Save()
+ {
+ // this code is duplicated in the other masters!!!!!!!!!!!!!
+ UnsavedValues = new Dictionary();
+ // 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;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/PepperDash.Core/Logging/CrestronEnricher.cs b/src/PepperDash.Core/Logging/CrestronEnricher.cs
new file mode 100644
index 00000000..902ce8d5
--- /dev/null
+++ b/src/PepperDash.Core/Logging/CrestronEnricher.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs
new file mode 100644
index 00000000..38dfa034
--- /dev/null
+++ b/src/PepperDash.Core/Logging/Debug.cs
@@ -0,0 +1,1028 @@
+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
+{
+ ///
+ /// Contains debug commands for use in various situations
+ ///
+ 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 _logLevels = new Dictionary()
+ {
+ {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; }
+ }
+
+ ///
+ /// 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.
+ ///
+ public static string OldFilePathPrefix = @"\nvram\debug\";
+
+ ///
+ /// 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.
+ ///
+ public static string NewFilePathPrefix = @"\user\debug\";
+
+ ///
+ /// The name of the file containing the current debug settings.
+ ///
+ public static string FileName = string.Format(@"app{0}Debug.json", InitialParametersClass.ApplicationNumber);
+
+ ///
+ /// Debug level to set for a given program.
+ ///
+ public static int Level { get; private set; }
+
+ ///
+ /// When this is true, the configuration file will NOT be loaded until triggered by either a console command or a signal
+ ///
+ public static bool DoNotLoadConfigOnNextBoot { get; private set; }
+
+ private static DebugContextCollection _contexts;
+
+ private const int SaveTimeoutMs = 30000;
+
+ public static bool IsRunningOnAppliance = CrestronEnvironment.DevicePlatform == eDevicePlatform.Appliance;
+
+ ///
+ /// Version for the currently loaded PepperDashCore dll
+ ///
+ public static string PepperDashCoreVersion { get; private set; }
+
+ private static CTimer _saveTimer;
+
+ ///
+ /// When true, the IncludedExcludedKeys dict will contain keys to include.
+ /// When false (default), IncludedExcludedKeys will contain keys to exclude.
+ ///
+ private static bool _excludeAllMode;
+
+ //static bool ExcludeNoKeyMessages;
+
+ private static readonly Dictionary 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();
+
+ 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) =>
+ {
+ LogMessage(LogEventLevel.Information, "Console debug level set to {minimumLevel}", _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();
+ }
+ }
+
+ ///
+ /// Used to save memory when shutting down
+ ///
+ ///
+ static void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ {
+
+ if (programEventType == eProgramStatusEventType.Stopping)
+ {
+ Log.CloseAndFlush();
+
+ if (_saveTimer != null)
+ {
+ _saveTimer.Stop();
+ _saveTimer = null;
+ }
+ LogMessage(LogEventLevel.Information, "Saving debug settings");
+ SaveMemory();
+ }
+ }
+
+ ///
+ /// Callback for console command
+ ///
+ ///
+ 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(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]");
+ }
+ }
+
+ ///
+ /// Sets the debug level
+ ///
+ /// Valid values 0-5
+ 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);
+ }
+
+ ///
+ /// Callback for console command
+ ///
+ ///
+ 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]");
+ }
+ }
+
+ ///
+ /// Callback for console command
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+ }
+ }
+
+
+
+
+ ///
+ /// sets the settings for a device or creates a new entry
+ ///
+ ///
+ ///
+ ///
+ public static void SetDeviceDebugSettings(string deviceKey, object settings)
+ {
+ _contexts.SetDebugSettingsForKey(deviceKey, settings);
+ SaveMemoryOnTimeout();
+ }
+
+ ///
+ /// Gets the device settings for a device by key or returns null
+ ///
+ ///
+ ///
+ public static object GetDeviceDebugSettingsForKey(string deviceKey)
+ {
+ return _contexts.GetDebugSettingsForKey(deviceKey);
+ }
+
+ ///
+ /// Sets the flag to prevent application starting on next boot
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ ///
+ ///
+ public static void ShowDebugLog(string s)
+ {
+ var loglist = CrestronLogger.PrintTheLog(s.ToLower() == "all");
+ foreach (var l in loglist)
+ CrestronConsole.ConsoleCommandResponse(l + CrestronEnvironment.NewLine);
+ }
+
+ ///
+ /// Log an Exception as an Error
+ ///
+ /// Exception to log
+ /// Message template
+ /// Optional IKeyed device. If provided, the Key of the device will be added to the log message
+ /// Args to put into message template
+ 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);
+ }
+ }
+
+ ///
+ /// Log a message
+ ///
+ /// Level to log at
+ /// Message template
+ /// Optional IKeyed device. If provided, the Key of the device will be added to the log message
+ /// Args to put into message template
+ 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)
+ {
+ _logger.Write(level, message, args);
+ }
+
+ public static void LogMessage(LogEventLevel level, Exception ex, string message, params object[] args)
+ {
+ _logger.Write(level, ex, message, args);
+ }
+
+ public static void LogMessage(LogEventLevel level, IKeyed keyed, string message, params object[] args)
+ {
+ LogMessage(level, message, keyed, args);
+ }
+
+ public static void LogMessage(LogEventLevel level, Exception ex, IKeyed device, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", device?.Key))
+ {
+ _logger.Write(level, ex, message, args);
+ }
+ }
+
+ #region Explicit methods for logging levels
+ public static void LogVerbose(IKeyed keyed, string message, params object[] args)
+ {
+ using(LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Verbose, message, args);
+ }
+ }
+
+ public static void LogVerbose(Exception ex, IKeyed keyed, string message, params object[] args)
+ {
+ using(LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Verbose, ex, message, args);
+ }
+ }
+
+ public static void LogVerbose(string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Verbose, message, args);
+ }
+
+ public static void LogVerbose(Exception ex, string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Verbose, ex, null, message, args);
+ }
+
+ public static void LogDebug(IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Debug, message, args);
+ }
+ }
+
+ public static void LogDebug(Exception ex, IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Debug, ex, message, args);
+ }
+ }
+
+ public static void LogDebug(string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Debug, message, args);
+ }
+
+ public static void LogDebug(Exception ex, string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Debug, ex, null, message, args);
+ }
+
+ public static void LogInformation(IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Information, message, args);
+ }
+ }
+
+ public static void LogInformation(Exception ex, IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Information, ex, message, args);
+ }
+ }
+
+ public static void LogInformation(string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Information, message, args);
+ }
+
+ public static void LogInformation(Exception ex, string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Information, ex, null, message, args);
+ }
+
+ public static void LogWarning(IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Warning, message, args);
+ }
+ }
+
+ public static void LogWarning(Exception ex, IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Warning, ex, message, args);
+ }
+ }
+
+ public static void LogWarning(string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Warning, message, args);
+ }
+
+ public static void LogWarning(Exception ex, string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Warning, ex, null, message, args);
+ }
+
+ public static void LogError(IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Error, message, args);
+ }
+ }
+
+ public static void LogError(Exception ex, IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Error, ex, message, args);
+ }
+ }
+
+ public static void LogError(string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Error, message, args);
+ }
+
+ public static void LogError(Exception ex, string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Error, ex, null, message, args);
+ }
+
+ public static void LogFatal(IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Fatal, message, args);
+ }
+ }
+
+ public static void LogFatal(Exception ex, IKeyed keyed, string message, params object[] args)
+ {
+ using (LogContext.PushProperty("Key", keyed?.Key))
+ {
+ _logger.Write(LogEventLevel.Fatal, ex, message, args);
+ }
+ }
+
+ public static void LogFatal(string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Fatal, message, args);
+ }
+
+ public static void LogFatal(Exception ex, string message, params object[] args)
+ {
+ _logger.Write(LogEventLevel.Fatal, ex, null, message, args);
+ }
+
+ #endregion
+
+
+ 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);
+ }
+
+
+ ///
+ /// Prints message to console if current debug level is equal to or higher than the level of this message.
+ /// Uses CrestronConsole.PrintLine.
+ ///
+ ///
+ /// Console format string
+ /// Object parameters
+ [Obsolete("Use LogMessage methods. Will be removed in 2.2.0 and later versions")]
+ 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));
+ //}
+ }
+
+ ///
+ /// Logs to Console when at-level, and all messages to error log, including device key
+ ///
+ [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")]
+ 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")]
+ public static void Console(uint level, IKeyed dev, ErrorLogLevel errorLogLevel,
+ string format, params object[] items)
+ {
+ LogMessage(level, dev, format, items);
+ }
+
+ ///
+ /// Logs to Console when at-level, and all messages to error log
+ ///
+ [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")]
+ public static void Console(uint level, ErrorLogLevel errorLogLevel,
+ string format, params object[] items)
+ {
+ LogMessage(level, format, items);
+ }
+
+ ///
+ /// 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.
+ ///
+ [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")]
+ 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);
+ }
+
+ ///
+ /// 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.
+ ///
+ [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")]
+ 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);
+ }
+
+ ///
+ /// Prints to log and error log
+ ///
+ ///
+ ///
+ [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")]
+ 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;
+ }
+ }
+
+ ///
+ /// Writes the memory object after timeout
+ ///
+ static void SaveMemoryOnTimeout()
+ {
+ Console(0, "Saving debug settings");
+ if (_saveTimer == null)
+ _saveTimer = new CTimer(o =>
+ {
+ _saveTimer = null;
+ SaveMemory();
+ }, SaveTimeoutMs);
+ else
+ _saveTimer.Reset(SaveTimeoutMs);
+ }
+
+ ///
+ /// Writes the memory - use SaveMemoryOnTimeout
+ ///
+ static void SaveMemory()
+ {
+ //var dir = @"\NVRAM\debug";
+ //if (!Directory.Exists(dir))
+ // Directory.Create(dir);
+
+ var fileName = GetMemoryFileName();
+
+ LogMessage(LogEventLevel.Information, "Loading debug settings file from {fileName}", fileName);
+
+ using (var sw = new StreamWriter(fileName))
+ {
+ var json = JsonConvert.SerializeObject(_contexts);
+ sw.Write(json);
+ sw.Flush();
+ }
+ }
+
+ ///
+ ///
+ ///
+ static void LoadMemory()
+ {
+ var file = GetMemoryFileName();
+ if (File.Exists(file))
+ {
+ using (var sr = new StreamReader(file))
+ {
+ var json = sr.ReadToEnd();
+ _contexts = JsonConvert.DeserializeObject(json);
+
+ if (_contexts != null)
+ {
+ LogMessage(LogEventLevel.Debug, "Debug memory restored from file");
+ return;
+ }
+ }
+ }
+
+ _contexts = new DebugContextCollection();
+ }
+
+ ///
+ /// Helper to get the file path for this app's debug memory
+ ///
+ 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);
+ }
+
+ ///
+ /// Error level to for message to be logged at
+ ///
+ public enum ErrorLogLevel
+ {
+ ///
+ /// Error
+ ///
+ Error,
+ ///
+ /// Warning
+ ///
+ Warning,
+ ///
+ /// Notice
+ ///
+ Notice,
+ ///
+ /// None
+ ///
+ None,
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Logging/DebugConsoleSink.cs b/src/PepperDash.Core/Logging/DebugConsoleSink.cs
new file mode 100644
index 00000000..a6c7f893
--- /dev/null
+++ b/src/PepperDash.Core/Logging/DebugConsoleSink.cs
@@ -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));
+ }
+ }
+
+}
diff --git a/src/PepperDash.Core/Logging/DebugContext.cs b/src/PepperDash.Core/Logging/DebugContext.cs
new file mode 100644
index 00000000..54c87414
--- /dev/null
+++ b/src/PepperDash.Core/Logging/DebugContext.cs
@@ -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
+{
+ ///
+ /// Represents a debugging context
+ ///
+ public class DebugContext
+ {
+ ///
+ /// 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.
+ ///
+ public string Key { get; private set; }
+
+ /////
+ ///// The name of the file containing the current debug settings.
+ /////
+ //string FileName = string.Format(@"\nvram\debug\app{0}Debug.json", InitialParametersClass.ApplicationNumber);
+
+ DebugContextSaveData SaveData;
+
+ int SaveTimeoutMs = 30000;
+
+ CTimer SaveTimer;
+
+
+ static List Contexts = new List();
+
+ ///
+ /// Creates or gets a debug context
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ /// Do not use. For S+ access.
+ ///
+ 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();
+ }
+
+ ///
+ /// Used to save memory when shutting down
+ ///
+ ///
+ void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ {
+ if (programEventType == eProgramStatusEventType.Stopping)
+ {
+ if (SaveTimer != null)
+ {
+ SaveTimer.Stop();
+ SaveTimer = null;
+ }
+ Console(0, "Saving debug settings");
+ SaveMemory();
+ }
+ }
+
+ ///
+ /// Callback for console command
+ ///
+ ///
+ 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]");
+ }
+ }
+
+ ///
+ /// Sets the debug level
+ ///
+ /// Valid values 0 (no debug), 1 (critical), 2 (all messages)
+ 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);
+ }
+ }
+
+ ///
+ /// Prints message to console if current debug level is equal to or higher than the level of this message.
+ /// Uses CrestronConsole.PrintLine.
+ ///
+ ///
+ /// Console format string
+ /// Object parameters
+ 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));
+ }
+
+ ///
+ /// Appends a device Key to the beginning of a message
+ ///
+ 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));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Writes the memory object after timeout
+ ///
+ void SaveMemoryOnTimeout()
+ {
+ if (SaveTimer == null)
+ SaveTimer = new CTimer(o =>
+ {
+ SaveTimer = null;
+ SaveMemory();
+ }, SaveTimeoutMs);
+ else
+ SaveTimer.Reset(SaveTimeoutMs);
+ }
+
+ ///
+ /// Writes the memory - use SaveMemoryOnTimeout
+ ///
+ void SaveMemory()
+ {
+ using (StreamWriter sw = new StreamWriter(GetMemoryFileName()))
+ {
+ var json = JsonConvert.SerializeObject(SaveData);
+ sw.Write(json);
+ sw.Flush();
+ }
+ }
+
+ ///
+ ///
+ ///
+ void LoadMemory()
+ {
+ var file = GetMemoryFileName();
+ if (File.Exists(file))
+ {
+ using (StreamReader sr = new StreamReader(file))
+ {
+ var data = JsonConvert.DeserializeObject(sr.ReadToEnd());
+ if (data != null)
+ {
+ SaveData = data;
+ Debug.Console(1, "Debug memory restored from file");
+ return;
+ }
+ else
+ SaveData = new DebugContextSaveData();
+ }
+ }
+ }
+
+ ///
+ /// Helper to get the file path for this app's debug memory
+ ///
+ string GetMemoryFileName()
+ {
+ return string.Format(@"\NVRAM\debugSettings\program{0}-{1}", InitialParametersClass.ApplicationNumber, Key);
+ }
+ }
+
+ ///
+ ///
+ ///
+ public class DebugContextSaveData
+ {
+ ///
+ ///
+ ///
+ public int Level { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Logging/DebugCrestronLoggerSink.cs b/src/PepperDash.Core/Logging/DebugCrestronLoggerSink.cs
new file mode 100644
index 00000000..0814453b
--- /dev/null
+++ b/src/PepperDash.Core/Logging/DebugCrestronLoggerSink.cs
@@ -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);
+ }
+ }
+}
diff --git a/src/PepperDash.Core/Logging/DebugErrorLogSink.cs b/src/PepperDash.Core/Logging/DebugErrorLogSink.cs
new file mode 100644
index 00000000..3885982b
--- /dev/null
+++ b/src/PepperDash.Core/Logging/DebugErrorLogSink.cs
@@ -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> _errorLogMap = new Dictionary>
+ {
+ { 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;
+ }
+ }
+}
diff --git a/src/PepperDash.Core/Logging/DebugExtensions.cs b/src/PepperDash.Core/Logging/DebugExtensions.cs
new file mode 100644
index 00000000..a8b7bd55
--- /dev/null
+++ b/src/PepperDash.Core/Logging/DebugExtensions.cs
@@ -0,0 +1,74 @@
+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, Exception ex, string message, params object[] args)
+ {
+ Log.LogMessage(LogEventLevel.Verbose, 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, Exception ex, string message, params object[] args)
+ {
+ Log.LogMessage(LogEventLevel.Debug, ex, message, device, 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, Exception ex, string message, params object[] args)
+ {
+ Log.LogMessage(LogEventLevel.Information, ex, message, device, 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, Exception ex, string message, params object[] args)
+ {
+ Log.LogMessage(LogEventLevel.Warning, ex, message, device, 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, Exception ex, string message, params object[] args)
+ {
+ Log.LogMessage(LogEventLevel.Error, ex, message, device, 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, Exception ex, string message, params object[] args)
+ {
+ Log.LogMessage(LogEventLevel.Fatal, ex, message, device, args);
+ }
+
+ public static void LogFatal(this IKeyed device, string message, params object[] args)
+ {
+ Log.LogMessage(LogEventLevel.Fatal, device, message, args);
+ }
+ }
+}
diff --git a/src/PepperDash.Core/Logging/DebugMemory.cs b/src/PepperDash.Core/Logging/DebugMemory.cs
new file mode 100644
index 00000000..a5737af9
--- /dev/null
+++ b/src/PepperDash.Core/Logging/DebugMemory.cs
@@ -0,0 +1,115 @@
+using System.Collections.Generic;
+using Crestron.SimplSharp;
+using Newtonsoft.Json;
+
+namespace PepperDash.Core.Logging
+{
+ ///
+ /// Class to persist current Debug settings across program restarts
+ ///
+ public class DebugContextCollection
+ {
+ ///
+ /// To prevent threading issues with the DeviceDebugSettings collection
+ ///
+ private readonly CCriticalSection _deviceDebugSettingsLock;
+
+ [JsonProperty("items")] private readonly Dictionary _items;
+
+ ///
+ /// Collection of the debug settings for each device where the dictionary key is the device key
+ ///
+ [JsonProperty("deviceDebugSettings")]
+ private Dictionary DeviceDebugSettings { get; set; }
+
+
+ ///
+ /// Default constructor
+ ///
+ public DebugContextCollection()
+ {
+ _deviceDebugSettingsLock = new CCriticalSection();
+ DeviceDebugSettings = new Dictionary();
+ _items = new Dictionary();
+ }
+
+ ///
+ /// Sets the level of a given context item, and adds that item if it does not
+ /// exist
+ ///
+ ///
+ ///
+ public void SetLevel(string contextKey, int level)
+ {
+ if (level < 0 || level > 2)
+ return;
+ GetOrCreateItem(contextKey).Level = level;
+ }
+
+ ///
+ /// Gets a level or creates it if not existing
+ ///
+ ///
+ ///
+ public DebugContextItem GetOrCreateItem(string contextKey)
+ {
+ if (!_items.ContainsKey(contextKey))
+ _items[contextKey] = new DebugContextItem { Level = 0 };
+ return _items[contextKey];
+ }
+
+
+ ///
+ /// sets the settings for a device or creates a new entry
+ ///
+ ///
+ ///
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Gets the device settings for a device by key or returns null
+ ///
+ ///
+ ///
+ public object GetDebugSettingsForKey(string deviceKey)
+ {
+ return DeviceDebugSettings[deviceKey];
+ }
+ }
+
+ ///
+ /// Contains information about
+ ///
+ public class DebugContextItem
+ {
+ ///
+ /// The level of debug messages to print
+ ///
+ [JsonProperty("level")]
+ public int Level { get; set; }
+
+ ///
+ /// Property to tell the program not to intitialize when it boots, if desired
+ ///
+ [JsonProperty("doNotLoadOnNextBoot")]
+ public bool DoNotLoadOnNextBoot { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
new file mode 100644
index 00000000..d818977e
--- /dev/null
+++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
@@ -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(_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);
+ }
+ }
+}
diff --git a/src/PepperDash.Core/Network/DiscoveryThings.cs b/src/PepperDash.Core/Network/DiscoveryThings.cs
new file mode 100644
index 00000000..973c03a4
--- /dev/null
+++ b/src/PepperDash.Core/Network/DiscoveryThings.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core
+{
+ ///
+ /// Not in use
+ ///
+ public static class NetworkComm
+ {
+ ///
+ /// Not in use
+ ///
+ static NetworkComm()
+ {
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/PasswordManagement/Config.cs b/src/PepperDash.Core/PasswordManagement/Config.cs
new file mode 100644
index 00000000..22aa4881
--- /dev/null
+++ b/src/PepperDash.Core/PasswordManagement/Config.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.PasswordManagement
+{
+ ///
+ /// JSON password configuration
+ ///
+ public class PasswordConfig
+ {
+ ///
+ /// Password object configured password
+ ///
+ public string password { get; set; }
+ ///
+ /// Constructor
+ ///
+ public PasswordConfig()
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/PasswordManagement/Constants.cs b/src/PepperDash.Core/PasswordManagement/Constants.cs
new file mode 100644
index 00000000..65a1bf45
--- /dev/null
+++ b/src/PepperDash.Core/PasswordManagement/Constants.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.PasswordManagement
+{
+ ///
+ /// Constants
+ ///
+ public class PasswordManagementConstants
+ {
+ ///
+ /// Generic boolean value change constant
+ ///
+ public const ushort BoolValueChange = 1;
+ ///
+ /// Evaluated boolean change constant
+ ///
+ public const ushort PasswordInitializedChange = 2;
+ ///
+ /// Update busy change const
+ ///
+ public const ushort PasswordUpdateBusyChange = 3;
+ ///
+ /// Password is valid change constant
+ ///
+ public const ushort PasswordValidationChange = 4;
+ ///
+ /// Password LED change constant
+ ///
+ public const ushort PasswordLedFeedbackChange = 5;
+
+ ///
+ /// Generic ushort value change constant
+ ///
+ public const ushort UshrtValueChange = 101;
+ ///
+ /// Password count
+ ///
+ public const ushort PasswordManagerCountChange = 102;
+ ///
+ /// Password selecte index change constant
+ ///
+ public const ushort PasswordSelectIndexChange = 103;
+ ///
+ /// Password length
+ ///
+ public const ushort PasswordLengthChange = 104;
+
+ ///
+ /// Generic string value change constant
+ ///
+ public const ushort StringValueChange = 201;
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/PasswordManagement/PasswordClient.cs b/src/PepperDash.Core/PasswordManagement/PasswordClient.cs
new file mode 100644
index 00000000..225a563c
--- /dev/null
+++ b/src/PepperDash.Core/PasswordManagement/PasswordClient.cs
@@ -0,0 +1,187 @@
+using System;
+
+namespace PepperDash.Core.PasswordManagement
+{
+ ///
+ /// A class to allow user interaction with the PasswordManager
+ ///
+ public class PasswordClient
+ {
+ ///
+ /// Password selected
+ ///
+ public string Password { get; set; }
+ ///
+ /// Password selected key
+ ///
+ public ushort Key { get; set; }
+ ///
+ /// Used to build the password entered by the user
+ ///
+ public string PasswordToValidate { get; set; }
+
+ ///
+ /// Boolean event
+ ///
+ public event EventHandler BoolChange;
+ ///
+ /// Ushort event
+ ///
+ public event EventHandler UshrtChange;
+ ///
+ /// String event
+ ///
+ public event EventHandler StringChange;
+
+ ///
+ /// Constructor
+ ///
+ public PasswordClient()
+ {
+ PasswordManager.PasswordChange += new EventHandler(PasswordManager_PasswordChange);
+ }
+
+ ///
+ /// Initialize method
+ ///
+ public void Initialize()
+ {
+ OnBoolChange(false, 0, PasswordManagementConstants.PasswordInitializedChange);
+
+ Password = "";
+ PasswordToValidate = "";
+
+ OnUshrtChange((ushort)PasswordManager.Passwords.Count, 0, PasswordManagementConstants.PasswordManagerCountChange);
+ OnBoolChange(true, 0, PasswordManagementConstants.PasswordInitializedChange);
+ }
+
+ ///
+ /// Retrieve password by index
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Password validation method
+ ///
+ ///
+ 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();
+ }
+
+ ///
+ /// 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
+ ///
+ ///
+ public void BuildPassword(string data)
+ {
+ PasswordToValidate = String.Concat(PasswordToValidate, data);
+ OnBoolChange(true, (ushort)PasswordToValidate.Length, PasswordManagementConstants.PasswordLedFeedbackChange);
+
+ if (PasswordToValidate.Length == Password.Length)
+ ValidatePassword(PasswordToValidate);
+ }
+
+ ///
+ /// Clears the user entered password and resets the LEDs
+ ///
+ public void ClearPassword()
+ {
+ PasswordToValidate = "";
+ OnBoolChange(false, (ushort)PasswordToValidate.Length, PasswordManagementConstants.PasswordLedFeedbackChange);
+ }
+
+ ///
+ /// Protected boolean change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Protected ushort change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Protected string change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// If password changes while selected change event will be notifed and update the client
+ ///
+ ///
+ ///
+ protected void PasswordManager_PasswordChange(object sender, StringChangeEventArgs args)
+ {
+ //throw new NotImplementedException();
+ if (Key == args.Index)
+ {
+ //PasswordSelectedKey = args.Index;
+ //PasswordSelected = args.StringValue;
+ GetPasswordByIndex(args.Index);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/PasswordManagement/PasswordManager.cs b/src/PepperDash.Core/PasswordManagement/PasswordManager.cs
new file mode 100644
index 00000000..d15ac1e1
--- /dev/null
+++ b/src/PepperDash.Core/PasswordManagement/PasswordManager.cs
@@ -0,0 +1,241 @@
+using System;
+using System.Collections.Generic;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.PasswordManagement
+{
+ ///
+ /// Allows passwords to be stored and managed
+ ///
+ public class PasswordManager
+ {
+ ///
+ /// Public dictionary of known passwords
+ ///
+ public static Dictionary Passwords = new Dictionary();
+ ///
+ /// Private dictionary, used when passwords are updated
+ ///
+ private Dictionary _passwords = new Dictionary();
+
+ ///
+ /// Timer used to wait until password changes have stopped before updating the dictionary
+ ///
+ CTimer PasswordTimer;
+ ///
+ /// Timer length
+ ///
+ public long PasswordTimerElapsedMs = 5000;
+
+ ///
+ /// Boolean event
+ ///
+ public event EventHandler BoolChange;
+ ///
+ /// Ushort event
+ ///
+ public event EventHandler UshrtChange;
+ ///
+ /// String event
+ ///
+ public event EventHandler StringChange;
+ ///
+ /// Event to notify clients of an updated password at the specified index (uint)
+ ///
+ public static event EventHandler PasswordChange;
+
+ ///
+ /// Constructor
+ ///
+ public PasswordManager()
+ {
+
+ }
+
+ ///
+ /// Initialize password manager
+ ///
+ public void Initialize()
+ {
+ if (Passwords == null)
+ Passwords = new Dictionary();
+
+ if (_passwords == null)
+ _passwords = new Dictionary();
+
+ OnBoolChange(true, 0, PasswordManagementConstants.PasswordInitializedChange);
+ }
+
+ ///
+ /// Updates password stored in the dictonary
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// CTimer callback function
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Method to change the default timer value, (default 5000ms/5s)
+ ///
+ ///
+ public void PasswordTimerMs(ushort time)
+ {
+ PasswordTimerElapsedMs = Convert.ToInt64(time);
+ }
+
+ ///
+ /// Helper method for debugging to see what passwords are in the lists
+ ///
+ 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);
+ }
+
+ ///
+ /// Protected boolean change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Protected ushort change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Protected string change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Protected password change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/PepperDash.Core.csproj b/src/PepperDash.Core/PepperDash.Core.csproj
new file mode 100644
index 00000000..53a3c78a
--- /dev/null
+++ b/src/PepperDash.Core/PepperDash.Core.csproj
@@ -0,0 +1,66 @@
+
+
+ Library
+
+
+ PepperDash.Core
+ PepperDashCore
+ net472
+ true
+ en
+ bin\$(Configuration)\
+ False
+ True
+ PepperDash Core
+ PepperDash Technologies
+ git
+ https://github.com/PepperDash/PepperDashCore
+ crestron;4series;
+ false
+ $(Version)
+ true
+
+
+ full
+ TRACE;DEBUG;SERIES4
+
+
+ pdbonly
+ bin\4Series\$(Configuration)\PepperDashCore.xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/PepperDash.Core/PepperDashCore.build/net472/PepperDashCore.props b/src/PepperDash.Core/PepperDashCore.build/net472/PepperDashCore.props
new file mode 100644
index 00000000..b28b35c5
--- /dev/null
+++ b/src/PepperDash.Core/PepperDashCore.build/net472/PepperDashCore.props
@@ -0,0 +1,18 @@
+
+
+ 2.0.0-local
+ PepperDash Technologies
+ PepperDash Technologies
+ PepperDash Essentials
+ Copyright © 2025
+ git
+ Crestron; 4series
+ ../../output
+ LICENSE.md
+ README.md
+
+
+
+
+
+
diff --git a/src/PepperDash.Core/PepperDashCore.build/net472/PepperDashCore.targets b/src/PepperDash.Core/PepperDashCore.build/net472/PepperDashCore.targets
new file mode 100644
index 00000000..d90a48ef
--- /dev/null
+++ b/src/PepperDash.Core/PepperDashCore.build/net472/PepperDashCore.targets
@@ -0,0 +1,57 @@
+
+
+
+ true
+ build;
+
+
+ true
+ build;
+
+
+ true
+ build;
+
+
+
+ $(TargetDir)$(TargetName).$(Version).$(TargetFramework).cplz
+
+
+ $(TargetDir)$(TargetName).$(Version).$(TargetFramework).cpz
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+
+
+
+
+
+
+
+ doNotUse
+
+
+
+
diff --git a/src/PepperDash.Core/Properties/ControlSystem.cfg b/src/PepperDash.Core/Properties/ControlSystem.cfg
new file mode 100644
index 00000000..f99176fb
--- /dev/null
+++ b/src/PepperDash.Core/Properties/ControlSystem.cfg
@@ -0,0 +1,7 @@
+
+
+ MC3 SSH
+ ssh 10.0.0.15
+ Program01
+ Internal Flash
+
\ No newline at end of file
diff --git a/src/PepperDash.Core/SystemInfo/EventArgs and Constants.cs b/src/PepperDash.Core/SystemInfo/EventArgs and Constants.cs
new file mode 100644
index 00000000..cc71e303
--- /dev/null
+++ b/src/PepperDash.Core/SystemInfo/EventArgs and Constants.cs
@@ -0,0 +1,264 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.SystemInfo
+{
+ ///
+ /// Constants
+ ///
+ public class SystemInfoConstants
+ {
+ ///
+ ///
+ ///
+ public const ushort BoolValueChange = 1;
+ ///
+ ///
+ ///
+ public const ushort CompleteBoolChange = 2;
+ ///
+ ///
+ ///
+ public const ushort BusyBoolChange = 3;
+
+ ///
+ ///
+ ///
+ public const ushort UshortValueChange = 101;
+
+ ///
+ ///
+ ///
+ public const ushort StringValueChange = 201;
+ ///
+ ///
+ ///
+ public const ushort ConsoleResponseChange = 202;
+ ///
+ ///
+ ///
+ public const ushort ProcessorUptimeChange = 203;
+ ///
+ ///
+ ///
+ public const ushort ProgramUptimeChange = 204;
+
+ ///
+ ///
+ ///
+ public const ushort ObjectChange = 301;
+ ///
+ ///
+ ///
+ public const ushort ProcessorConfigChange = 302;
+ ///
+ ///
+ ///
+ public const ushort EthernetConfigChange = 303;
+ ///
+ ///
+ ///
+ public const ushort ControlSubnetConfigChange = 304;
+ ///
+ ///
+ ///
+ public const ushort ProgramConfigChange = 305;
+ }
+
+ ///
+ /// Processor Change Event Args Class
+ ///
+ public class ProcessorChangeEventArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public ProcessorInfo Processor { get; set; }
+ ///
+ ///
+ ///
+ public ushort Type { get; set; }
+ ///
+ ///
+ ///
+ public ushort Index { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public ProcessorChangeEventArgs()
+ {
+
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ public ProcessorChangeEventArgs(ProcessorInfo processor, ushort type)
+ {
+ Processor = processor;
+ Type = type;
+ }
+
+ ///
+ /// Constructor
+ ///
+ public ProcessorChangeEventArgs(ProcessorInfo processor, ushort type, ushort index)
+ {
+ Processor = processor;
+ Type = type;
+ Index = index;
+ }
+ }
+
+ ///
+ /// Ethernet Change Event Args Class
+ ///
+ public class EthernetChangeEventArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public EthernetInfo Adapter { get; set; }
+ ///
+ ///
+ ///
+ public ushort Type { get; set; }
+ ///
+ ///
+ ///
+ public ushort Index { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public EthernetChangeEventArgs()
+ {
+
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ public EthernetChangeEventArgs(EthernetInfo ethernet, ushort type)
+ {
+ Adapter = ethernet;
+ Type = type;
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ ///
+ public EthernetChangeEventArgs(EthernetInfo ethernet, ushort type, ushort index)
+ {
+ Adapter = ethernet;
+ Type = type;
+ Index = index;
+ }
+ }
+
+ ///
+ /// Control Subnet Chage Event Args Class
+ ///
+ public class ControlSubnetChangeEventArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public ControlSubnetInfo Adapter { get; set; }
+ ///
+ ///
+ ///
+ public ushort Type { get; set; }
+ ///
+ ///
+ ///
+ public ushort Index { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public ControlSubnetChangeEventArgs()
+ {
+
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ public ControlSubnetChangeEventArgs(ControlSubnetInfo controlSubnet, ushort type)
+ {
+ Adapter = controlSubnet;
+ Type = type;
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ public ControlSubnetChangeEventArgs(ControlSubnetInfo controlSubnet, ushort type, ushort index)
+ {
+ Adapter = controlSubnet;
+ Type = type;
+ Index = index;
+ }
+ }
+
+ ///
+ /// Program Change Event Args Class
+ ///
+ public class ProgramChangeEventArgs : EventArgs
+ {
+ ///
+ ///
+ ///
+ public ProgramInfo Program { get; set; }
+ ///
+ ///
+ ///
+ public ushort Type { get; set; }
+ ///
+ ///
+ ///
+ public ushort Index { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public ProgramChangeEventArgs()
+ {
+
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ public ProgramChangeEventArgs(ProgramInfo program, ushort type)
+ {
+ Program = program;
+ Type = type;
+ }
+
+ ///
+ /// Constructor overload
+ ///
+ ///
+ ///
+ ///
+ public ProgramChangeEventArgs(ProgramInfo program, ushort type, ushort index)
+ {
+ Program = program;
+ Type = type;
+ Index = index;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/SystemInfo/SystemInfoConfig.cs b/src/PepperDash.Core/SystemInfo/SystemInfoConfig.cs
new file mode 100644
index 00000000..8dc3acaf
--- /dev/null
+++ b/src/PepperDash.Core/SystemInfo/SystemInfoConfig.cs
@@ -0,0 +1,204 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.SystemInfo
+{
+ ///
+ /// Processor info class
+ ///
+ public class ProcessorInfo
+ {
+ ///
+ ///
+ ///
+ public string Model { get; set; }
+ ///
+ ///
+ ///
+ public string SerialNumber { get; set; }
+ ///
+ ///
+ ///
+ public string Firmware { get; set; }
+ ///
+ ///
+ ///
+ public string FirmwareDate { get; set; }
+ ///
+ ///
+ ///
+ public string OsVersion { get; set; }
+ ///
+ ///
+ ///
+ public string RuntimeEnvironment { get; set; }
+ ///
+ ///
+ ///
+ public string DevicePlatform { get; set; }
+ ///
+ ///
+ ///
+ public string ModuleDirectory { get; set; }
+ ///
+ ///
+ ///
+ public string LocalTimeZone { get; set; }
+ ///
+ ///
+ ///
+ public string ProgramIdTag { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public ProcessorInfo()
+ {
+
+ }
+ }
+
+ ///
+ /// Ethernet info class
+ ///
+ public class EthernetInfo
+ {
+ ///
+ ///
+ ///
+ public ushort DhcpIsOn { get; set; }
+ ///
+ ///
+ ///
+ public string Hostname { get; set; }
+ ///
+ ///
+ ///
+ public string MacAddress { get; set; }
+ ///
+ ///
+ ///
+ public string IpAddress { get; set; }
+ ///
+ ///
+ ///
+ public string Subnet { get; set; }
+ ///
+ ///
+ ///
+ public string Gateway { get; set; }
+ ///
+ ///
+ ///
+ public string Dns1 { get; set; }
+ ///
+ ///
+ ///
+ public string Dns2 { get; set; }
+ ///
+ ///
+ ///
+ public string Dns3 { get; set; }
+ ///
+ ///
+ ///
+ public string Domain { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public EthernetInfo()
+ {
+
+ }
+ }
+
+ ///
+ /// Control subnet info class
+ ///
+ public class ControlSubnetInfo
+ {
+ ///
+ ///
+ ///
+ public ushort Enabled { get; set; }
+ ///
+ ///
+ ///
+ public ushort IsInAutomaticMode { get; set; }
+ ///
+ ///
+ ///
+ public string MacAddress { get; set; }
+ ///
+ ///
+ ///
+ public string IpAddress { get; set; }
+ ///
+ ///
+ ///
+ public string Subnet { get; set; }
+ ///
+ ///
+ ///
+ public string RouterPrefix { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public ControlSubnetInfo()
+ {
+
+ }
+ }
+
+ ///
+ /// Program info class
+ ///
+ public class ProgramInfo
+ {
+ ///
+ ///
+ ///
+ public string Name { get; set; }
+ ///
+ ///
+ ///
+ public string Header { get; set; }
+ ///
+ ///
+ ///
+ public string System { get; set; }
+ ///
+ ///
+ ///
+ public string ProgramIdTag { get; set; }
+ ///
+ ///
+ ///
+ public string CompileTime { get; set; }
+ ///
+ ///
+ ///
+ public string Database { get; set; }
+ ///
+ ///
+ ///
+ public string Environment { get; set; }
+ ///
+ ///
+ ///
+ public string Programmer { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public ProgramInfo()
+ {
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/SystemInfo/SystemInfoToSimpl.cs b/src/PepperDash.Core/SystemInfo/SystemInfoToSimpl.cs
new file mode 100644
index 00000000..6677b9ef
--- /dev/null
+++ b/src/PepperDash.Core/SystemInfo/SystemInfoToSimpl.cs
@@ -0,0 +1,462 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.SystemInfo
+{
+ ///
+ /// System Info class
+ ///
+ public class SystemInfoToSimpl
+ {
+ ///
+ /// Notifies of bool change
+ ///
+ public event EventHandler BoolChange;
+ ///
+ /// Notifies of string change
+ ///
+ public event EventHandler StringChange;
+
+ ///
+ /// Notifies of processor change
+ ///
+ public event EventHandler ProcessorChange;
+ ///
+ /// Notifies of ethernet change
+ ///
+ public event EventHandler EthernetChange;
+ ///
+ /// Notifies of control subnet change
+ ///
+ public event EventHandler ControlSubnetChange;
+ ///
+ /// Notifies of program change
+ ///
+ public event EventHandler ProgramChange;
+
+ ///
+ /// Constructor
+ ///
+ public SystemInfoToSimpl()
+ {
+
+ }
+
+ ///
+ /// Gets the current processor info
+ ///
+ 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);
+ }
+
+ ///
+ /// Gets the current ethernet info
+ ///
+ 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);
+ }
+
+ ///
+ /// Gets the current control subnet info
+ ///
+ 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);
+ }
+
+ ///
+ /// Gets the program info by index
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Gets the processor uptime and passes it to S+
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Gets the program uptime, by index, and passes it to S+
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Sends command to console, passes response back using string change event
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// private method to parse console messages
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ /// Protected boolean change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Protected string change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Protected processor config change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Ethernet change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Control Subnet change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Program change event handler
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Web/BouncyCertificate.cs b/src/PepperDash.Core/Web/BouncyCertificate.cs
new file mode 100644
index 00000000..bf8b0f4c
--- /dev/null
+++ b/src/PepperDash.Core/Web/BouncyCertificate.cs
@@ -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
+{
+ ///
+ /// Taken From https://github.com/rlipscombe/bouncy-castle-csharp/
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ private BigInteger GenerateSerialNumber(SecureRandom random)
+ {
+ var serialNumber =
+ BigIntegers.CreateRandomInRange(
+ BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
+ return serialNumber;
+ }
+
+ ///
+ /// Generate a key pair.
+ ///
+ /// The random number generator.
+ /// The key length in bits. For RSA, 2048 bits should be considered the minimum acceptable these days.
+ ///
+ 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;
+ }
+
+ ///
+ /// 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.
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Add the "Subject Alternative Names" extension. Note that you have to repeat
+ /// the value from the "Subject Name" property.
+ ///
+ ///
+ ///
+ private void AddSubjectAlternativeNames(X509V3CertificateGenerator certificateGenerator,
+ IEnumerable subjectAlternativeNames)
+ {
+ var subjectAlternativeNamesExtension =
+ new DerSequence(
+ subjectAlternativeNames.Select(name => new GeneralName(GeneralName.DnsName, name))
+ .ToArray());
+ certificateGenerator.AddExtension(
+ X509Extensions.SubjectAlternativeName.Id, false, subjectAlternativeNamesExtension);
+ }
+
+ ///
+ /// Add the "Extended Key Usage" extension, specifying (for example) "server authentication".
+ ///
+ ///
+ ///
+ private void AddExtendedKeyUsage(X509V3CertificateGenerator certificateGenerator, KeyPurposeID[] usages)
+ {
+ certificateGenerator.AddExtension(
+ X509Extensions.ExtendedKeyUsage.Id, false, new ExtendedKeyUsage(usages));
+ }
+
+ ///
+ /// Add the "Basic Constraints" extension.
+ ///
+ ///
+ ///
+ private void AddBasicConstraints(X509V3CertificateGenerator certificateGenerator,
+ bool isCertificateAuthority)
+ {
+ certificateGenerator.AddExtension(
+ X509Extensions.BasicConstraints.Id, true, new BasicConstraints(isCertificateAuthority));
+ }
+
+ ///
+ /// Add the Subject Key Identifier.
+ ///
+ ///
+ ///
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Web/RequestHandlers/DefaultRequestHandler.cs b/src/PepperDash.Core/Web/RequestHandlers/DefaultRequestHandler.cs
new file mode 100644
index 00000000..ca19cf2f
--- /dev/null
+++ b/src/PepperDash.Core/Web/RequestHandlers/DefaultRequestHandler.cs
@@ -0,0 +1,17 @@
+using Crestron.SimplSharp.WebScripting;
+
+namespace PepperDash.Core.Web.RequestHandlers
+{
+ ///
+ /// Web API default request handler
+ ///
+ public class DefaultRequestHandler : WebApiBaseRequestHandler
+ {
+ ///
+ /// Constructor
+ ///
+ public DefaultRequestHandler()
+ : base(true)
+ { }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs b/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs
new file mode 100644
index 00000000..b1170031
--- /dev/null
+++ b/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs
@@ -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> _handlers;
+ protected readonly bool EnableCors;
+
+ ///
+ /// Constructor
+ ///
+ protected WebApiBaseRequestAsyncHandler(bool enableCors)
+ {
+ EnableCors = enableCors;
+
+ _handlers = new Dictionary>
+ {
+ {"CONNECT", HandleConnect},
+ {"DELETE", HandleDelete},
+ {"GET", HandleGet},
+ {"HEAD", HandleHead},
+ {"OPTIONS", HandleOptions},
+ {"PATCH", HandlePatch},
+ {"POST", HandlePost},
+ {"PUT", HandlePut},
+ {"TRACE", HandleTrace}
+ };
+ }
+
+ ///
+ /// Constructor
+ ///
+ protected WebApiBaseRequestAsyncHandler()
+ : this(false)
+ {
+ }
+
+ ///
+ /// Handles CONNECT method requests
+ ///
+ ///
+ protected virtual async Task HandleConnect(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles DELETE method requests
+ ///
+ ///
+ protected virtual async Task HandleDelete(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles GET method requests
+ ///
+ ///
+ protected virtual async Task HandleGet(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles HEAD method requests
+ ///
+ ///
+ protected virtual async Task HandleHead(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles OPTIONS method requests
+ ///
+ ///
+ protected virtual async Task HandleOptions(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles PATCH method requests
+ ///
+ ///
+ protected virtual async Task HandlePatch(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles POST method requests
+ ///
+ ///
+ protected virtual async Task HandlePost(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles PUT method requests
+ ///
+ ///
+ protected virtual async Task HandlePut(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles TRACE method requests
+ ///
+ ///
+ protected virtual async Task HandleTrace(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Process request
+ ///
+ ///
+ public void ProcessRequest(HttpCwsContext context)
+ {
+ if (!_handlers.TryGetValue(context.Request.HttpMethod, out Func 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();
+ }
+ }
+}
diff --git a/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs b/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs
new file mode 100644
index 00000000..99e4aa93
--- /dev/null
+++ b/src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs
@@ -0,0 +1,165 @@
+using System;
+using System.Collections.Generic;
+using Crestron.SimplSharp.WebScripting;
+
+namespace PepperDash.Core.Web.RequestHandlers
+{
+ ///
+ /// CWS Base Handler, implements IHttpCwsHandler
+ ///
+ public abstract class WebApiBaseRequestHandler : IHttpCwsHandler
+ {
+ private readonly Dictionary> _handlers;
+ protected readonly bool EnableCors;
+
+ ///
+ /// Constructor
+ ///
+ protected WebApiBaseRequestHandler(bool enableCors)
+ {
+ EnableCors = enableCors;
+
+ _handlers = new Dictionary>
+ {
+ {"CONNECT", HandleConnect},
+ {"DELETE", HandleDelete},
+ {"GET", HandleGet},
+ {"HEAD", HandleHead},
+ {"OPTIONS", HandleOptions},
+ {"PATCH", HandlePatch},
+ {"POST", HandlePost},
+ {"PUT", HandlePut},
+ {"TRACE", HandleTrace}
+ };
+ }
+
+ ///
+ /// Constructor
+ ///
+ protected WebApiBaseRequestHandler()
+ : this(false)
+ {
+ }
+
+ ///
+ /// Handles CONNECT method requests
+ ///
+ ///
+ protected virtual void HandleConnect(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles DELETE method requests
+ ///
+ ///
+ protected virtual void HandleDelete(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles GET method requests
+ ///
+ ///
+ protected virtual void HandleGet(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles HEAD method requests
+ ///
+ ///
+ protected virtual void HandleHead(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles OPTIONS method requests
+ ///
+ ///
+ protected virtual void HandleOptions(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles PATCH method requests
+ ///
+ ///
+ protected virtual void HandlePatch(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles POST method requests
+ ///
+ ///
+ protected virtual void HandlePost(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles PUT method requests
+ ///
+ ///
+ protected virtual void HandlePut(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Handles TRACE method requests
+ ///
+ ///
+ protected virtual void HandleTrace(HttpCwsContext context)
+ {
+ context.Response.StatusCode = 501;
+ context.Response.StatusDescription = "Not Implemented";
+ context.Response.End();
+ }
+
+ ///
+ /// Process request
+ ///
+ ///
+ public void ProcessRequest(HttpCwsContext context)
+ {
+ Action 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/Web/WebApiServer.cs b/src/PepperDash.Core/Web/WebApiServer.cs
new file mode 100644
index 00000000..cf45b361
--- /dev/null
+++ b/src/PepperDash.Core/Web/WebApiServer.cs
@@ -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
+{
+ ///
+ /// Web API server
+ ///
+ 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;
+
+ ///
+ /// Web API server key
+ ///
+ public string Key { get; private set; }
+
+ ///
+ /// Web API server name
+ ///
+ public string Name { get; private set; }
+
+ ///
+ /// CWS base path, will default to "/api" if not set via initialize method
+ ///
+ public string BasePath { get; private set; }
+
+ ///
+ /// Indicates CWS is registered with base path
+ ///
+ public bool IsRegistered { get; private set; }
+
+ ///
+ /// Http request handler
+ ///
+ //public IHttpCwsHandler HttpRequestHandler
+ //{
+ // get { return _server.HttpRequestHandler; }
+ // set
+ // {
+ // if (_server == null) return;
+ // _server.HttpRequestHandler = value;
+ // }
+ //}
+
+ ///
+ /// Received request event handler
+ ///
+ //public event EventHandler ReceivedRequestEvent
+ //{
+ // add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }
+ // remove { _server.ReceivedRequestEvent -= new HttpCwsRequestEventHandler(value); }
+ //}
+
+ ///
+ /// Constructor for S+. Make sure to set necessary properties using init method
+ ///
+ public WebApiServer()
+ : this(SplusKey, DefaultName, null)
+ {
+ }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ public WebApiServer(string key, string basePath)
+ : this(key, DefaultName, basePath)
+ {
+ }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ /// Program status event handler
+ ///
+ ///
+ void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType)
+ {
+ if (programEventType != eProgramStatusEventType.Stopping) return;
+
+ Debug.Console(DebugInfo, this, "Program stopping. stopping server");
+
+ Stop();
+ }
+
+ ///
+ /// Ethernet event handler
+ ///
+ ///
+ 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();
+ }
+
+ ///
+ /// Initializes CWS class
+ ///
+ public void Initialize(string key, string basePath)
+ {
+ Key = key;
+ BasePath = string.IsNullOrEmpty(basePath) ? DefaultBasePath : basePath;
+ }
+
+ ///
+ /// Adds a route to CWS
+ ///
+ 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);
+
+ }
+
+ ///
+ /// Removes a route from CWS
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Returns a list of the current routes
+ ///
+ public HttpCwsRouteCollection GetRouteCollection()
+ {
+ return _server.Routes;
+ }
+
+ ///
+ /// Starts CWS instance
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Stop CWS instance
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Received request handler
+ ///
+ ///
+ /// This is here for development and testing
+ ///
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/WebApi/Presets/Preset.cs b/src/PepperDash.Core/WebApi/Presets/Preset.cs
new file mode 100644
index 00000000..bdbc5820
--- /dev/null
+++ b/src/PepperDash.Core/WebApi/Presets/Preset.cs
@@ -0,0 +1,87 @@
+using System;
+
+namespace PepperDash.Core.WebApi.Presets
+{
+ ///
+ /// Represents a preset
+ ///
+ public class Preset
+ {
+ ///
+ /// ID of preset
+ ///
+ public int Id { get; set; }
+
+ ///
+ /// User ID
+ ///
+ public int UserId { get; set; }
+
+ ///
+ /// Room Type ID
+ ///
+ public int RoomTypeId { get; set; }
+
+ ///
+ /// Preset Name
+ ///
+ public string PresetName { get; set; }
+
+ ///
+ /// Preset Number
+ ///
+ public int PresetNumber { get; set; }
+
+ ///
+ /// Preset Data
+ ///
+ public string Data { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ public Preset()
+ {
+ PresetName = "";
+ PresetNumber = 1;
+ Data = "{}";
+ }
+ }
+
+ ///
+ ///
+ ///
+ public class PresetReceivedEventArgs : EventArgs
+ {
+ ///
+ /// True when the preset is found
+ ///
+ public bool LookupSuccess { get; private set; }
+
+ ///
+ /// S+ helper
+ ///
+ public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
+
+ ///
+ /// The preset
+ ///
+ public Preset Preset { get; private set; }
+
+ ///
+ /// For Simpl+
+ ///
+ public PresetReceivedEventArgs() { }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ public PresetReceivedEventArgs(Preset preset, bool success)
+ {
+ LookupSuccess = success;
+ Preset = preset;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/WebApi/Presets/User.cs b/src/PepperDash.Core/WebApi/Presets/User.cs
new file mode 100644
index 00000000..c82824f6
--- /dev/null
+++ b/src/PepperDash.Core/WebApi/Presets/User.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+namespace PepperDash.Core.WebApi.Presets
+{
+ ///
+ ///
+ ///
+ public class User
+ {
+ ///
+ ///
+ ///
+ public int Id { get; set; }
+
+ ///
+ ///
+ ///
+ public string ExternalId { get; set; }
+
+ ///
+ ///
+ ///
+ public string FirstName { get; set; }
+
+ ///
+ ///
+ ///
+ public string LastName { get; set; }
+ }
+
+
+ ///
+ ///
+ ///
+ public class UserReceivedEventArgs : EventArgs
+ {
+ ///
+ /// True when user is found
+ ///
+ public bool LookupSuccess { get; private set; }
+
+ ///
+ /// For stupid S+
+ ///
+ public ushort ULookupSuccess { get { return (ushort)(LookupSuccess ? 1 : 0); } }
+
+ ///
+ ///
+ ///
+ public User User { get; private set; }
+
+ ///
+ /// For Simpl+
+ ///
+ public UserReceivedEventArgs() { }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ public UserReceivedEventArgs(User user, bool success)
+ {
+ LookupSuccess = success;
+ User = user;
+ }
+ }
+
+ ///
+ ///
+ ///
+ public class UserAndRoomMessage
+ {
+ ///
+ ///
+ ///
+ public int UserId { get; set; }
+
+ ///
+ ///
+ ///
+ public int RoomTypeId { get; set; }
+
+ ///
+ ///
+ ///
+ public int PresetNumber { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs b/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs
new file mode 100644
index 00000000..0a9317bf
--- /dev/null
+++ b/src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs
@@ -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
+{
+ ///
+ /// Passcode client for the WebApi
+ ///
+ public class WebApiPasscodeClient : IKeyed
+ {
+ ///
+ /// Notifies when user received
+ ///
+ public event EventHandler UserReceived;
+
+ ///
+ /// Notifies when Preset received
+ ///
+ public event EventHandler PresetReceived;
+
+ ///
+ /// Unique identifier for this instance
+ ///
+ public string Key { get; private set; }
+
+ //string JsonMasterKey;
+
+ ///
+ /// An embedded JsonToSimpl master object.
+ ///
+ JsonToSimplGenericMaster J2SMaster;
+
+ string UrlBase;
+
+ string DefaultPresetJsonFilePath;
+
+ User CurrentUser;
+
+ Preset CurrentPreset;
+
+
+ ///
+ /// SIMPL+ can only execute the default constructor. If you have variables that require initialization, please
+ /// use an Initialize method
+ ///
+ public WebApiPasscodeClient()
+ {
+ }
+
+ ///
+ /// Initializes the instance
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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);
+ }
+
+ ///
+ /// Gets the user for a passcode
+ ///
+ ///
+ 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(resp.ContentString);
+ CurrentUser = user;
+ if (handler != null)
+ UserReceived(this, new UserReceivedEventArgs(user, true));
+ }
+ else
+ if (handler != null)
+ UserReceived(this, new UserReceivedEventArgs(null, false));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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(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);
+ }
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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
+ }
+
+ ///
+ /// After save operation on JSON master happens, send it to server
+ ///
+ ///
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/PepperDash.Core/XSigUtility/Serialization/IXSigSerialization.cs b/src/PepperDash.Core/XSigUtility/Serialization/IXSigSerialization.cs
new file mode 100644
index 00000000..8303731e
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/Serialization/IXSigSerialization.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using PepperDash.Core.Intersystem.Tokens;
+
+namespace PepperDash.Core.Intersystem.Serialization
+{
+ ///
+ /// Interface to determine XSig serialization for an object.
+ ///
+ public interface IXSigSerialization
+ {
+ ///
+ /// Serialize the sig data
+ ///
+ ///
+ IEnumerable Serialize();
+
+ ///
+ /// Deserialize the sig data
+ ///
+ ///
+ ///
+ ///
+ T Deserialize(IEnumerable tokens) where T : class, IXSigSerialization;
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/XSigUtility/Serialization/XSigSerializationException.cs b/src/PepperDash.Core/XSigUtility/Serialization/XSigSerializationException.cs
new file mode 100644
index 00000000..8f3fc047
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/Serialization/XSigSerializationException.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace PepperDash.Core.Intersystem.Serialization
+{
+ ///
+ /// Class to handle this specific exception type
+ ///
+ public class XSigSerializationException : Exception
+ {
+ ///
+ /// default constructor
+ ///
+ public XSigSerializationException() { }
+
+ ///
+ /// constructor with message
+ ///
+ ///
+ public XSigSerializationException(string message) : base(message) { }
+
+ ///
+ /// constructor with message and innner exception
+ ///
+ ///
+ ///
+ public XSigSerializationException(string message, Exception inner) : base(message, inner) { }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigAnalogToken.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigAnalogToken.cs
new file mode 100644
index 00000000..58473362
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigAnalogToken.cs
@@ -0,0 +1,88 @@
+using System;
+
+namespace PepperDash.Core.Intersystem.Tokens
+{
+ ///
+ /// Represents an XSigAnalogToken
+ ///
+ public sealed class XSigAnalogToken : XSigToken, IFormattable
+ {
+ private readonly ushort _value;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ ///
+ ///
+ public ushort Value
+ {
+ get { return _value; }
+ }
+
+ ///
+ ///
+ ///
+ public override XSigTokenType TokenType
+ {
+ get { return XSigTokenType.Analog; }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ 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)
+ };
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override XSigToken GetTokenWithOffset(int offset)
+ {
+ if (offset == 0) return this;
+ return new XSigAnalogToken(Index + offset, Value);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override string ToString()
+ {
+ return Index + " = 0x" + Value.ToString("X4");
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public string ToString(string format, IFormatProvider formatProvider)
+ {
+ return Value.ToString(format, formatProvider);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigDigitalToken.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigDigitalToken.cs
new file mode 100644
index 00000000..70ccc852
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigDigitalToken.cs
@@ -0,0 +1,85 @@
+using System;
+
+namespace PepperDash.Core.Intersystem.Tokens
+{
+ ///
+ /// Represents an XSigDigitalToken
+ ///
+ public sealed class XSigDigitalToken : XSigToken
+ {
+ private readonly bool _value;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ ///
+ ///
+ public bool Value
+ {
+ get { return _value; }
+ }
+
+ ///
+ ///
+ ///
+ public override XSigTokenType TokenType
+ {
+ get { return XSigTokenType.Digital; }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override byte[] GetBytes()
+ {
+ return new[] {
+ (byte)(0x80 | (Value ? 0 : 0x20) | ((Index - 1) >> 7)),
+ (byte)((Index - 1) & 0x7F)
+ };
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override XSigToken GetTokenWithOffset(int offset)
+ {
+ if (offset == 0) return this;
+ return new XSigDigitalToken(Index + offset, Value);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override string ToString()
+ {
+ return Index + " = " + (Value ? "High" : "Low");
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public string ToString(IFormatProvider formatProvider)
+ {
+ return Value.ToString(formatProvider);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigSerialToken.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigSerialToken.cs
new file mode 100644
index 00000000..25ee3fd0
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigSerialToken.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Text;
+
+namespace PepperDash.Core.Intersystem.Tokens
+{
+ ///
+ /// Represents an XSigSerialToken
+ ///
+ public sealed class XSigSerialToken : XSigToken
+ {
+ private readonly string _value;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ ///
+ ///
+ public string Value
+ {
+ get { return _value; }
+ }
+
+ ///
+ ///
+ ///
+ public override XSigTokenType TokenType
+ {
+ get { return XSigTokenType.Serial; }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ 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;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ public override XSigToken GetTokenWithOffset(int offset)
+ {
+ if (offset == 0) return this;
+ return new XSigSerialToken(Index + offset, Value);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public override string ToString()
+ {
+ return Index + " = \"" + Value + "\"";
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigToken.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigToken.cs
new file mode 100644
index 00000000..4c00a2ed
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigToken.cs
@@ -0,0 +1,45 @@
+namespace PepperDash.Core.Intersystem.Tokens
+{
+ ///
+ /// Represents the base class for all XSig datatypes.
+ ///
+ public abstract class XSigToken
+ {
+ private readonly int _index;
+
+ ///
+ /// Constructs an XSigToken with the specified index.
+ ///
+ /// Index for the data.
+ protected XSigToken(int index)
+ {
+ _index = index;
+ }
+
+ ///
+ /// XSig 1-based index.
+ ///
+ public int Index
+ {
+ get { return _index; }
+ }
+
+ ///
+ /// XSigToken type.
+ ///
+ public abstract XSigTokenType TokenType { get; }
+
+ ///
+ /// Generates the XSig bytes for the corresponding token.
+ ///
+ /// XSig byte array.
+ public abstract byte[] GetBytes();
+
+ ///
+ /// Returns a new token if necessary with an updated index based on the specified offset.
+ ///
+ /// Offset to adjust the index with.
+ /// XSigToken
+ public abstract XSigToken GetTokenWithOffset(int offset);
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/XSigUtility/Tokens/XSigTokenType.cs b/src/PepperDash.Core/XSigUtility/Tokens/XSigTokenType.cs
new file mode 100644
index 00000000..26d6c123
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/Tokens/XSigTokenType.cs
@@ -0,0 +1,23 @@
+namespace PepperDash.Core.Intersystem.Tokens
+{
+ ///
+ /// XSig token types.
+ ///
+ public enum XSigTokenType
+ {
+ ///
+ /// Digital signal datatype.
+ ///
+ Digital,
+
+ ///
+ /// Analog signal datatype.
+ ///
+ Analog,
+
+ ///
+ /// Serial signal datatype.
+ ///
+ Serial
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/XSigUtility/XSigHelpers.cs b/src/PepperDash.Core/XSigUtility/XSigHelpers.cs
new file mode 100644
index 00000000..4ea6f634
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/XSigHelpers.cs
@@ -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
+{
+ ///
+ /// Helper methods for creating XSig byte sequences compatible with the Intersystem Communications (ISC) symbol.
+ ///
+ ///
+ /// 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.
+ ///
+ public static class XSigHelpers
+ {
+ ///
+ /// Forces all outputs to 0.
+ ///
+ /// Bytes in XSig format for clear outputs trigger.
+ public static byte[] ClearOutputs()
+ {
+ return new byte[] { 0xFC };
+ }
+
+ ///
+ /// Evaluate all inputs and re-transmit any digital, analog, and permanent serail signals not set to 0.
+ ///
+ /// Bytes in XSig format for send status trigger.
+ public static byte[] SendStatus()
+ {
+ return new byte[] { 0xFD };
+ }
+
+ ///
+ /// Get bytes for an IXSigStateResolver object.
+ ///
+ /// XSig state resolver.
+ /// Bytes in XSig format for each token within the state representation.
+ public static byte[] GetBytes(IXSigSerialization xSigSerialization)
+ {
+ return GetBytes(xSigSerialization, 0);
+ }
+
+ ///
+ /// Get bytes for an IXSigStateResolver object, with a specified offset.
+ ///
+ /// XSig state resolver.
+ /// Offset to which the data will be aligned.
+ /// Bytes in XSig format for each token within the state representation.
+ 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();
+ }
+ }
+
+ ///
+ /// Get bytes for a single digital signal.
+ ///
+ /// 1-based digital index
+ /// Digital data to be encoded
+ /// Bytes in XSig format for digtial information.
+ public static byte[] GetBytes(int index, bool value)
+ {
+ return GetBytes(index, 0, value);
+ }
+
+ ///
+ /// Get bytes for a single digital signal.
+ ///
+ /// 1-based digital index
+ /// Index offset.
+ /// Digital data to be encoded
+ /// Bytes in XSig format for digtial information.
+ public static byte[] GetBytes(int index, int offset, bool value)
+ {
+ return new XSigDigitalToken(index + offset, value).GetBytes();
+ }
+
+ ///
+ /// Get byte sequence for multiple digital signals.
+ ///
+ /// Starting index of the sequence.
+ /// Digital signal value array.
+ /// Byte sequence in XSig format for digital signal information.
+ public static byte[] GetBytes(int startIndex, bool[] values)
+ {
+ return GetBytes(startIndex, 0, values);
+ }
+
+ ///
+ /// Get byte sequence for multiple digital signals.
+ ///
+ /// Starting index of the sequence.
+ /// Index offset.
+ /// Digital signal value array.
+ /// Byte sequence in XSig format for digital signal information.
+ 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;
+ }
+
+ ///
+ /// Get bytes for a single analog signal.
+ ///
+ /// 1-based analog index
+ /// Analog data to be encoded
+ /// Bytes in XSig format for analog signal information.
+ public static byte[] GetBytes(int index, ushort value)
+ {
+ return GetBytes(index, 0, value);
+ }
+
+ ///
+ /// Get bytes for a single analog signal.
+ ///
+ /// 1-based analog index
+ /// Index offset.
+ /// Analog data to be encoded
+ /// Bytes in XSig format for analog signal information.
+ public static byte[] GetBytes(int index, int offset, ushort value)
+ {
+ return new XSigAnalogToken(index + offset, value).GetBytes();
+ }
+
+ ///
+ /// Get byte sequence for multiple analog signals.
+ ///
+ /// Starting index of the sequence.
+ /// Analog signal value array.
+ /// Byte sequence in XSig format for analog signal information.
+ public static byte[] GetBytes(int startIndex, ushort[] values)
+ {
+ return GetBytes(startIndex, 0, values);
+ }
+
+ ///
+ /// Get byte sequence for multiple analog signals.
+ ///
+ /// Starting index of the sequence.
+ /// Index offset.
+ /// Analog signal value array.
+ /// Byte sequence in XSig format for analog signal information.
+ 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;
+ }
+
+ ///
+ /// Get bytes for a single serial signal.
+ ///
+ /// 1-based serial index
+ /// Serial data to be encoded
+ /// Bytes in XSig format for serial signal information.
+ public static byte[] GetBytes(int index, string value)
+ {
+ return GetBytes(index, 0, value);
+ }
+
+ ///
+ /// Get bytes for a single serial signal.
+ ///
+ /// 1-based serial index
+ /// Index offset.
+ /// Serial data to be encoded
+ /// Bytes in XSig format for serial signal information.
+ public static byte[] GetBytes(int index, int offset, string value)
+ {
+ return new XSigSerialToken(index + offset, value).GetBytes();
+ }
+
+ ///
+ /// Get byte sequence for multiple serial signals.
+ ///
+ /// Starting index of the sequence.
+ /// Serial signal value array.
+ /// Byte sequence in XSig format for serial signal information.
+ public static byte[] GetBytes(int startIndex, string[] values)
+ {
+ return GetBytes(startIndex, 0, values);
+ }
+
+ ///
+ /// Get byte sequence for multiple serial signals.
+ ///
+ /// Starting index of the sequence.
+ /// Index offset.
+ /// Serial signal value array.
+ /// Byte sequence in XSig format for serial signal information.
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/XSigUtility/XSigTokenStreamReader.cs b/src/PepperDash.Core/XSigUtility/XSigTokenStreamReader.cs
new file mode 100644
index 00000000..9d70d02e
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/XSigTokenStreamReader.cs
@@ -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
+{
+ ///
+ /// XSigToken stream reader.
+ ///
+ public sealed class XSigTokenStreamReader : IDisposable
+ {
+ private readonly Stream _stream;
+ private readonly bool _leaveOpen;
+
+ ///
+ ///
+ /// XSigToken stream reader constructor.
+ ///
+ /// Input stream to read from.
+ /// Stream is null.
+ /// Stream cannot be read from.
+ public XSigTokenStreamReader(Stream stream)
+ : this(stream, false) { }
+
+ ///
+ /// XSigToken stream reader constructor.
+ ///
+ /// Input stream to read from.
+ /// Determines whether to leave the stream open or not.
+ /// Stream is null.
+ /// Stream cannot be read from.
+ 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;
+ }
+
+ ///
+ /// Reads a 16-bit unsigned integer from the specified stream using Big Endian byte order.
+ ///
+ /// Input stream
+ /// Result
+ /// True if successful, otherwise false.
+ 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;
+ }
+
+ ///
+ /// Read XSig token from the stream.
+ ///
+ /// XSigToken
+ /// Offset is less than 0.
+ 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;
+ }
+
+ ///
+ /// Reads all available XSig tokens from the stream.
+ ///
+ /// XSigToken collection.
+ public IEnumerable ReadAllXSigTokens()
+ {
+ var tokens = new List();
+ XSigToken token;
+ while ((token = ReadXSigToken()) != null)
+ tokens.Add(token);
+
+ return tokens;
+ }
+
+ ///
+ /// Attempts to deserialize all XSig data within the stream from the current position.
+ ///
+ /// Type to deserialize the information to.
+ /// Deserialized object.
+ public T DeserializeStream()
+ where T : class, IXSigSerialization, new()
+ {
+ return new T().Deserialize(ReadAllXSigTokens());
+ }
+
+ ///
+ /// Disposes of the internal stream if specified to not leave open.
+ ///
+ public void Dispose()
+ {
+ if (!_leaveOpen)
+ _stream.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Core/XSigUtility/XSigTokenStreamWriter.cs b/src/PepperDash.Core/XSigUtility/XSigTokenStreamWriter.cs
new file mode 100644
index 00000000..934f2c29
--- /dev/null
+++ b/src/PepperDash.Core/XSigUtility/XSigTokenStreamWriter.cs
@@ -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
+{
+ ///
+ /// XSigToken stream writer.
+ ///
+ public sealed class XSigTokenStreamWriter : IDisposable
+ {
+ private readonly Stream _stream;
+ private readonly bool _leaveOpen;
+
+ ///
+ ///
+ /// XSigToken stream writer constructor.
+ ///
+ /// Input stream to write to.
+ /// Stream is null.
+ /// Stream cannot be written to.
+ public XSigTokenStreamWriter(Stream stream)
+ : this(stream, false) { }
+
+ ///
+ /// XSigToken stream writer constructor.
+ ///
+ /// Input stream to write to.
+ /// Determines whether to leave the stream open or not.
+ /// Stream is null.
+ /// Stream cannot be written to.
+ 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;
+ }
+
+ ///
+ /// Write XSig data gathered from an IXSigStateResolver to the stream.
+ ///
+ /// IXSigStateResolver object.
+ public void WriteXSigData(IXSigSerialization xSigSerialization)
+ {
+ WriteXSigData(xSigSerialization, 0);
+ }
+
+ ///
+ /// Write XSig data gathered from an IXSigStateResolver to the stream.
+ ///
+ /// IXSigStateResolver object.
+ /// Index offset for each XSigToken.
+ public void WriteXSigData(IXSigSerialization xSigSerialization, int offset)
+ {
+ if (xSigSerialization == null)
+ throw new ArgumentNullException("xSigSerialization");
+
+ var tokens = xSigSerialization.Serialize();
+ WriteXSigData(tokens, offset);
+ }
+
+ ///
+ /// Write XSigToken to the stream.
+ ///
+ /// XSigToken object.
+ public void WriteXSigData(XSigToken token)
+ {
+ WriteXSigData(token, 0);
+ }
+
+ ///
+ /// Write XSigToken to the stream.
+ ///
+ /// XSigToken object.
+ /// Index offset for each XSigToken.
+ public void WriteXSigData(XSigToken token, int offset)
+ {
+ WriteXSigData(new[] { token }, offset);
+ }
+
+ ///
+ /// Writes an array of XSigTokens to the stream.
+ ///
+ /// XSigToken objects.
+ public void WriteXSigData(XSigToken[] tokens)
+ {
+ WriteXSigData(tokens.AsEnumerable());
+ }
+
+ ///
+ /// Write an enumerable collection of XSigTokens to the stream.
+ ///
+ /// XSigToken objects.
+ public void WriteXSigData(IEnumerable tokens)
+ {
+ WriteXSigData(tokens, 0);
+ }
+
+ ///
+ /// Write an enumerable collection of XSigTokens to the stream.
+ ///
+ /// XSigToken objects.
+ /// Index offset for each XSigToken.
+ public void WriteXSigData(IEnumerable 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);
+ }
+ }
+ }
+
+ ///
+ /// Disposes of the internal stream if specified to not leave open.
+ ///
+ public void Dispose()
+ {
+ if (!_leaveOpen)
+ _stream.Dispose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs b/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs
index 47632c8d..d88e9728 100644
--- a/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs
+++ b/src/PepperDash.Essentials.Core/Comm and IR/GenericHttpClient.cs
@@ -7,17 +7,19 @@ namespace PepperDash.Essentials.Core
[Obsolete("Please use the builtin HttpClient class instead: https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines")]
public class GenericHttpClient : Device, IBasicCommunication
{
- public HttpClient Client;
+ private readonly HttpClient Client;
public event EventHandler ResponseRecived;
public GenericHttpClient(string key, string name, string hostname)
: base(key, name)
{
- Client = new HttpClient();
- Client.HostName = hostname;
-
-
- }
+ Client = new HttpClient
+ {
+ HostName = hostname
+ };
+
+
+ }
///
///
@@ -54,9 +56,8 @@ namespace PepperDash.Essentials.Core
if (responseReceived.ContentString.Length > 0)
{
- if (ResponseRecived != null)
- ResponseRecived(this, new GenericHttpClientEventArgs(responseReceived.ContentString, (request as HttpClientRequest).Url.ToString(), error));
- }
+ ResponseRecived?.Invoke(this, new GenericHttpClientEventArgs(responseReceived.ContentString, (request as HttpClientRequest).Url.ToString(), error));
+ }
}
}
diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs
new file mode 100644
index 00000000..9b82cacd
--- /dev/null
+++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs
@@ -0,0 +1,8 @@
+using PepperDash.Core;
+
+namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
+{
+ public interface IDisplay: IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking, IKeyName
+ {
+ }
+}
diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasInputs.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasInputs.cs
index 9fa0f222..ff9511fa 100644
--- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasInputs.cs
+++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IHasInputs.cs
@@ -1,27 +1,7 @@
using PepperDash.Core;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
-
- ///
- /// Describes a device that has selectable inputs
- ///
- /// the type to use as the key for each input item. Most likely an enum or string\
- ///
- /// See MockDisplay for example implemntation
- ///
- [Obsolete("Use IHasInputs instead. Will be removed for 2.0 release")]
- public interface IHasInputs: IKeyName
- {
- ISelectableItems Inputs { get; }
- }
-
-
///
/// Describes a device that has selectable inputs
///
diff --git a/src/PepperDash.Essentials.Core/Devices/FIND HOMES Interfaces.cs b/src/PepperDash.Essentials.Core/Devices/FIND HOMES Interfaces.cs
index 318f5179..d4b09779 100644
--- a/src/PepperDash.Essentials.Core/Devices/FIND HOMES Interfaces.cs
+++ b/src/PepperDash.Essentials.Core/Devices/FIND HOMES Interfaces.cs
@@ -1,12 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Crestron.SimplSharp;
-using Crestron.SimplSharpPro;
-using Crestron.SimplSharpPro.DeviceSupport;
-
-using PepperDash.Core;
+using PepperDash.Core;
namespace PepperDash.Essentials.Core
@@ -16,22 +8,6 @@ namespace PepperDash.Essentials.Core
BoolFeedback IsOnline { get; }
}
- /////
- ///// ** WANT THIS AND ALL ITS FRIENDS TO GO AWAY **
- ///// Defines a class that has a list of CueAction objects, typically
- ///// for linking functions to user interfaces or API calls
- /////
- //public interface IHasCueActionList
- //{
- // List CueActionList { get; }
- //}
-
-
- //public interface IHasComPortsHardware
- //{
- // IComPorts ComPortsDevice { get; }
- //}
-
///
/// Describes a device that can have a video sync providing device attached to it
///
diff --git a/src/PepperDash.Essentials.Core/Devices/PC/Laptop.cs b/src/PepperDash.Essentials.Core/Devices/PC/Laptop.cs
deleted file mode 100644
index 921d52c2..00000000
--- a/src/PepperDash.Essentials.Core/Devices/PC/Laptop.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Crestron.SimplSharpPro;
-
-using PepperDash.Essentials.Core;
-using PepperDash.Essentials.Core.Config;
-using PepperDash.Core;
-using Serilog.Events;
-
-namespace PepperDash.Essentials.Core.Devices
-{
-
- [Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")]
- public class Laptop : EssentialsDevice, IHasFeedback, IRoutingOutputs, IAttachVideoStatus, IUiDisplayInfo, IUsageTracking
- {
- public uint DisplayUiType { get { return DisplayUiConstants.TypeLaptop; } }
- public string IconName { get; set; }
- public BoolFeedback HasPowerOnFeedback { get; private set; }
-
- public RoutingOutputPort AnyVideoOut { get; private set; }
-
- #region IRoutingOutputs Members
-
- ///
- /// Options: hdmi
- ///
- public RoutingPortCollection OutputPorts { get; private set; }
-
- #endregion
-
- public Laptop(string key, string name)
- : base(key, name)
- {
- IconName = "Laptop";
- HasPowerOnFeedback = new BoolFeedback("HasPowerFeedback",
- () => this.GetVideoStatuses() != VideoStatusOutputs.NoStatus);
- OutputPorts = new RoutingPortCollection();
- OutputPorts.Add(AnyVideoOut = new RoutingOutputPort(RoutingPortNames.AnyOut, eRoutingSignalType.Audio | eRoutingSignalType.Video,
- eRoutingPortConnectionType.None, 0, this));
- }
-
- #region IHasFeedback Members
-
- ///
- /// Passes through the VideoStatuses list
- ///
- public FeedbackCollection Feedbacks
- {
- get
- {
- var newList = new FeedbackCollection();
- newList.AddRange(this.GetVideoStatuses().ToList());
- return newList;
- }
- }
-
- #endregion
-
- #region IUsageTracking Members
-
- public UsageTracking UsageTracker { get; set; }
-
- #endregion
- }
-
- [Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")]
- public class LaptopFactory : EssentialsDeviceFactory
- {
- public LaptopFactory()
- {
- TypeNames = new List() { "deprecated" };
- }
-
- public override EssentialsDevice BuildDevice(DeviceConfig dc)
- {
- Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Laptop Device");
- return new Core.Devices.Laptop(dc.Key, dc.Name);
- }
- }
-}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Display/BasicIrDisplay.cs b/src/PepperDash.Essentials.Core/Display/BasicIrDisplay.cs
deleted file mode 100644
index 394fa17a..00000000
--- a/src/PepperDash.Essentials.Core/Display/BasicIrDisplay.cs
+++ /dev/null
@@ -1,229 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Crestron.SimplSharp;
-using Crestron.SimplSharpPro;
-using Crestron.SimplSharpPro.DeviceSupport;
-
-using PepperDash.Core;
-using PepperDash.Essentials.Core;
-using PepperDash.Essentials.Core.Config;
-using PepperDash.Essentials.Core.Bridges;
-using Serilog.Events;
-
-namespace PepperDash.Essentials.Core
-{
- [Obsolete("Please use PepperDash.Essentials.Device.Common, this will be removed in 2.1")]
- public class BasicIrDisplay : DisplayBase, IBasicVolumeControls, IBridgeAdvanced
- {
- public IrOutputPortController IrPort { get; private set; }
- public ushort IrPulseTime { get; set; }
-
- protected Func PowerIsOnFeedbackFunc
- {
- get { return () => _PowerIsOn; }
- }
- protected override Func IsCoolingDownFeedbackFunc
- {
- get { return () => _IsCoolingDown; }
- }
- protected override Func IsWarmingUpFeedbackFunc
- {
- get { return () => _IsWarmingUp; }
- }
-
- bool _PowerIsOn;
- bool _IsWarmingUp;
- bool _IsCoolingDown;
-
- public BasicIrDisplay(string key, string name, IROutputPort port, string irDriverFilepath)
- : base(key, name)
- {
- IrPort = new IrOutputPortController(key + "-ir", port, irDriverFilepath);
- DeviceManager.AddDevice(IrPort);
-
- IsWarmingUpFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Verbose, this, "Warming up={0}", _IsWarmingUp);
- IsCoolingDownFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Verbose, this, "Cooling down={0}", _IsCoolingDown);
-
- InputPorts.AddRange(new RoutingPortCollection
- {
- new RoutingInputPort(RoutingPortNames.HdmiIn1, eRoutingSignalType.Audio | eRoutingSignalType.Video,
- eRoutingPortConnectionType.Hdmi, new Action(Hdmi1), this, false),
- new RoutingInputPort(RoutingPortNames.HdmiIn2, eRoutingSignalType.Audio | eRoutingSignalType.Video,
- eRoutingPortConnectionType.Hdmi, new Action(Hdmi2), this, false),
- new RoutingInputPort(RoutingPortNames.HdmiIn3, eRoutingSignalType.Audio | eRoutingSignalType.Video,
- eRoutingPortConnectionType.Hdmi, new Action(Hdmi3), this, false),
- new RoutingInputPort(RoutingPortNames.HdmiIn4, eRoutingSignalType.Audio | eRoutingSignalType.Video,
- eRoutingPortConnectionType.Hdmi, new Action(Hdmi4), this, false),
- new RoutingInputPort(RoutingPortNames.ComponentIn, eRoutingSignalType.Audio | eRoutingSignalType.Video,
- eRoutingPortConnectionType.Hdmi, new Action(Component1), this, false),
- new RoutingInputPort(RoutingPortNames.CompositeIn, eRoutingSignalType.Audio | eRoutingSignalType.Video,
- eRoutingPortConnectionType.Hdmi, new Action(Video1), this, false),
- new RoutingInputPort(RoutingPortNames.AntennaIn, eRoutingSignalType.Audio | eRoutingSignalType.Video,
- eRoutingPortConnectionType.Hdmi, new Action(Antenna), this, false),
- });
- }
-
- public void Hdmi1()
- {
- IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_1, IrPulseTime);
- }
-
- public void Hdmi2()
- {
- IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_2, IrPulseTime);
- }
-
- public void Hdmi3()
- {
- IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_3, IrPulseTime);
- }
-
- public void Hdmi4()
- {
- IrPort.Pulse(IROutputStandardCommands.IROut_HDMI_4, IrPulseTime);
- }
-
- public void Component1()
- {
- IrPort.Pulse(IROutputStandardCommands.IROut_COMPONENT_1, IrPulseTime);
- }
-
- public void Video1()
- {
- IrPort.Pulse(IROutputStandardCommands.IROut_VIDEO_1, IrPulseTime);
- }
-
- public void Antenna()
- {
- IrPort.Pulse(IROutputStandardCommands.IROut_ANTENNA, IrPulseTime);
- }
-
- #region IPower Members
-
- public override void PowerOn()
- {
- IrPort.Pulse(IROutputStandardCommands.IROut_POWER_ON, IrPulseTime);
- _PowerIsOn = true;
- }
-
- public override void PowerOff()
- {
- _PowerIsOn = false;
- IrPort.Pulse(IROutputStandardCommands.IROut_POWER_OFF, IrPulseTime);
- }
-
- public override void PowerToggle()
- {
- _PowerIsOn = false;
- IrPort.Pulse(IROutputStandardCommands.IROut_POWER, IrPulseTime);
- }
-
- #endregion
-
- #region IBasicVolumeControls Members
-
- public void VolumeUp(bool pressRelease)
- {
- IrPort.PressRelease(IROutputStandardCommands.IROut_VOL_PLUS, pressRelease);
- }
-
- public void VolumeDown(bool pressRelease)
- {
- IrPort.PressRelease(IROutputStandardCommands.IROut_VOL_MINUS, pressRelease);
- }
-
- public void MuteToggle()
- {
- IrPort.Pulse(IROutputStandardCommands.IROut_MUTE, 200);
- }
-
- #endregion
-
- void StartWarmingTimer()
- {
- _IsWarmingUp = true;
- IsWarmingUpFeedback.FireUpdate();
- new CTimer(o => {
- _IsWarmingUp = false;
- IsWarmingUpFeedback.FireUpdate();
- }, 10000);
- }
-
- void StartCoolingTimer()
- {
- _IsCoolingDown = true;
- IsCoolingDownFeedback.FireUpdate();
- new CTimer(o =>
- {
- _IsCoolingDown = false;
- IsCoolingDownFeedback.FireUpdate();
- }, 7000);
- }
-
- #region IRoutingSink Members
-
- ///
- /// Typically called by the discovery routing algorithm.
- ///
- /// A delegate containing the input selector method to call
- public override void ExecuteSwitch(object inputSelector)
- {
- Debug.LogMessage(LogEventLevel.Verbose, this, "Switching to input '{0}'", (inputSelector as Action).ToString());
-
- Action finishSwitch = () =>
- {
- var action = inputSelector as Action;
- if (action != null)
- action();
- };
-
- if (!_PowerIsOn)
- {
- PowerOn();
- EventHandler oneTimer = null;
- oneTimer = (o, a) =>
- {
- if (IsWarmingUpFeedback.BoolValue) return; // Only catch done warming
- IsWarmingUpFeedback.OutputChange -= oneTimer;
- finishSwitch();
- };
- IsWarmingUpFeedback.OutputChange += oneTimer;
- }
- else // Do it!
- finishSwitch();
- }
-
- #endregion
-
- public void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge)
- {
- LinkDisplayToApi(this, trilist, joinStart, joinMapKey, bridge);
- }
- }
-
- [Obsolete("Please use PepperDash.Essentials.Device.Common, this will be removed in 2.1")]
- public class BasicIrDisplayFactory : EssentialsDeviceFactory
- {
- public BasicIrDisplayFactory()
- {
- TypeNames = new List() { "basicirdisplay" };
- }
-
- public override EssentialsDevice BuildDevice(DeviceConfig dc)
- {
- Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new BasicIrDisplay Device");
- var ir = IRPortHelper.GetIrPort(dc.Properties);
- if (ir != null)
- {
- var display = new BasicIrDisplay(dc.Key, dc.Name, ir.Port, ir.FileName);
- display.IrPulseTime = 200; // Set default pulse time for IR commands.
- return display;
- }
-
- return null;
- }
- }
-
-}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Display/DisplayBase.cs b/src/PepperDash.Essentials.Core/Display/DisplayBase.cs
deleted file mode 100644
index a41f936f..00000000
--- a/src/PepperDash.Essentials.Core/Display/DisplayBase.cs
+++ /dev/null
@@ -1,320 +0,0 @@
-
-
-using Crestron.SimplSharp;
-using Crestron.SimplSharpPro.DeviceSupport;
-using Newtonsoft.Json;
-using PepperDash.Core;
-using PepperDash.Essentials.Core.Bridges;
-using Serilog.Events;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-
-namespace PepperDash.Essentials.Core
-{
- [Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")]
- public abstract class DisplayBase : EssentialsDevice, IHasFeedback, IRoutingSinkWithSwitching, IHasPowerControl, IWarmingCooling, IUsageTracking
- {
- public event SourceInfoChangeHandler CurrentSourceChange;
- public event InputChangedEventHandler InputChanged;
-
- public string CurrentSourceInfoKey { get; set; }
- public SourceListItem CurrentSourceInfo
- {
- get
- {
- return _CurrentSourceInfo;
- }
- set
- {
- if (value == _CurrentSourceInfo) return;
-
- var handler = CurrentSourceChange;
-
- if (handler != null)
- handler(_CurrentSourceInfo, ChangeType.WillChange);
-
- _CurrentSourceInfo = value;
-
- if (handler != null)
- handler(_CurrentSourceInfo, ChangeType.DidChange);
- }
- }
- SourceListItem _CurrentSourceInfo;
-
- public BoolFeedback IsCoolingDownFeedback { get; protected set; }
- public BoolFeedback IsWarmingUpFeedback { get; private set; }
-
- public UsageTracking UsageTracker { get; set; }
-
- public uint WarmupTime { get; set; }
- public uint CooldownTime { get; set; }
-
- ///
- /// Bool Func that will provide a value for the PowerIsOn Output. Must be implemented
- /// by concrete sub-classes
- ///
- abstract protected Func IsCoolingDownFeedbackFunc { get; }
- abstract protected Func IsWarmingUpFeedbackFunc { get; }
-
-
- protected CTimer WarmupTimer;
- protected CTimer CooldownTimer;
-
- #region IRoutingInputs Members
-
- public RoutingPortCollection InputPorts { get; private set; }
-
- #endregion
-
- protected DisplayBase(string key, string name)
- : base(key, name)
- {
- IsCoolingDownFeedback = new BoolFeedback("IsCoolingDown", IsCoolingDownFeedbackFunc);
- IsWarmingUpFeedback = new BoolFeedback("IsWarmingUp", IsWarmingUpFeedbackFunc);
-
- InputPorts = new RoutingPortCollection();
-
- }
-
- public abstract void PowerOn();
- public abstract void PowerOff();
- public abstract void PowerToggle();
-
- public virtual FeedbackCollection Feedbacks
- {
- get
- {
- return new FeedbackCollection
- {
- IsCoolingDownFeedback,
- IsWarmingUpFeedback
- };
- }
- }
-
- public RoutingInputPort CurrentInputPort => throw new NotImplementedException();
-
- public abstract void ExecuteSwitch(object selector);
-
- protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, uint joinStart, string joinMapKey,
- EiscApiAdvanced bridge)
- {
- var joinMap = new DisplayControllerJoinMap(joinStart);
-
- var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey);
-
- if (!string.IsNullOrEmpty(joinMapSerialized))
- joinMap = JsonConvert.DeserializeObject(joinMapSerialized);
-
- if (bridge != null)
- {
- bridge.AddJoinMap(Key, joinMap);
- }
- else
- {
- Debug.LogMessage(LogEventLevel.Information,this,"Please update config to use 'eiscapiadvanced' to get all join map features for this device.");
- }
-
- LinkDisplayToApi(displayDevice, trilist, joinMap);
- }
-
- protected void LinkDisplayToApi(DisplayBase displayDevice, BasicTriList trilist, DisplayControllerJoinMap joinMap)
- {
- Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X"));
- Debug.LogMessage(LogEventLevel.Information, "Linking to Display: {0}", displayDevice.Name);
-
- trilist.StringInput[joinMap.Name.JoinNumber].StringValue = displayDevice.Name;
-
- var commMonitor = displayDevice as ICommunicationMonitor;
- if (commMonitor != null)
- {
- commMonitor.CommunicationMonitor.IsOnlineFeedback.LinkInputSig(trilist.BooleanInput[joinMap.IsOnline.JoinNumber]);
- }
-
- var inputNumber = 0;
- var inputKeys = new List();
-
- var inputNumberFeedback = new IntFeedback(() => inputNumber);
-
- // Two way feedbacks
- var twoWayDisplay = displayDevice as TwoWayDisplayBase;
-
- if (twoWayDisplay != null)
- {
- trilist.SetBool(joinMap.IsTwoWayDisplay.JoinNumber, true);
-
- twoWayDisplay.CurrentInputFeedback.OutputChange += (o, a) => Debug.LogMessage(LogEventLevel.Information, "CurrentInputFeedback_OutputChange {0}", a.StringValue);
-
-
- inputNumberFeedback.LinkInputSig(trilist.UShortInput[joinMap.InputSelect.JoinNumber]);
- }
-
- // Power Off
- trilist.SetSigTrueAction(joinMap.PowerOff.JoinNumber, () =>
- {
- inputNumber = 102;
- inputNumberFeedback.FireUpdate();
- displayDevice.PowerOff();
- });
-
- var twoWayDisplayDevice = displayDevice as TwoWayDisplayBase;
- if (twoWayDisplayDevice != null)
- {
- twoWayDisplayDevice.PowerIsOnFeedback.OutputChange += (o, a) =>
- {
- if (!a.BoolValue)
- {
- inputNumber = 102;
- inputNumberFeedback.FireUpdate();
-
- }
- else
- {
- inputNumber = 0;
- inputNumberFeedback.FireUpdate();
- }
- };
-
- twoWayDisplayDevice.PowerIsOnFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.PowerOff.JoinNumber]);
- twoWayDisplayDevice.PowerIsOnFeedback.LinkInputSig(trilist.BooleanInput[joinMap.PowerOn.JoinNumber]);
- }
-
- // PowerOn
- trilist.SetSigTrueAction(joinMap.PowerOn.JoinNumber, () =>
- {
- inputNumber = 0;
- inputNumberFeedback.FireUpdate();
- displayDevice.PowerOn();
- });
-
-
-
- for (int i = 0; i < displayDevice.InputPorts.Count; i++)
- {
- if (i < joinMap.InputNamesOffset.JoinSpan)
- {
- inputKeys.Add(displayDevice.InputPorts[i].Key);
- var tempKey = inputKeys.ElementAt(i);
- trilist.SetSigTrueAction((ushort)(joinMap.InputSelectOffset.JoinNumber + i),
- () => displayDevice.ExecuteSwitch(displayDevice.InputPorts[tempKey].Selector));
- Debug.LogMessage(LogEventLevel.Verbose, displayDevice, "Setting Input Select Action on Digital Join {0} to Input: {1}",
- joinMap.InputSelectOffset.JoinNumber + i, displayDevice.InputPorts[tempKey].Key.ToString());
- trilist.StringInput[(ushort)(joinMap.InputNamesOffset.JoinNumber + i)].StringValue = displayDevice.InputPorts[i].Key.ToString();
- }
- else
- Debug.LogMessage(LogEventLevel.Information, displayDevice, "Device has {0} inputs. The Join Map allows up to {1} inputs. Discarding inputs {2} - {3} from bridge.",
- displayDevice.InputPorts.Count, joinMap.InputNamesOffset.JoinSpan, i + 1, displayDevice.InputPorts.Count);
- }
-
- Debug.LogMessage(LogEventLevel.Verbose, displayDevice, "Setting Input Select Action on Analog Join {0}", joinMap.InputSelect);
- trilist.SetUShortSigAction(joinMap.InputSelect.JoinNumber, (a) =>
- {
- if (a == 0)
- {
- displayDevice.PowerOff();
- inputNumber = 0;
- }
- else if (a > 0 && a < displayDevice.InputPorts.Count && a != inputNumber)
- {
- displayDevice.ExecuteSwitch(displayDevice.InputPorts.ElementAt(a - 1).Selector);
- inputNumber = a;
- }
- else if (a == 102)
- {
- displayDevice.PowerToggle();
-
- }
- if (twoWayDisplay != null)
- inputNumberFeedback.FireUpdate();
- });
-
-
- var volumeDisplay = displayDevice as IBasicVolumeControls;
- if (volumeDisplay == null) return;
-
- trilist.SetBoolSigAction(joinMap.VolumeUp.JoinNumber, volumeDisplay.VolumeUp);
- trilist.SetBoolSigAction(joinMap.VolumeDown.JoinNumber, volumeDisplay.VolumeDown);
- trilist.SetSigTrueAction(joinMap.VolumeMute.JoinNumber, volumeDisplay.MuteToggle);
-
- var volumeDisplayWithFeedback = volumeDisplay as IBasicVolumeWithFeedback;
-
- if (volumeDisplayWithFeedback == null) return;
- trilist.SetSigTrueAction(joinMap.VolumeMuteOn.JoinNumber, volumeDisplayWithFeedback.MuteOn);
- trilist.SetSigTrueAction(joinMap.VolumeMuteOff.JoinNumber, volumeDisplayWithFeedback.MuteOff);
-
-
- trilist.SetUShortSigAction(joinMap.VolumeLevel.JoinNumber, volumeDisplayWithFeedback.SetVolume);
- volumeDisplayWithFeedback.VolumeLevelFeedback.LinkInputSig(trilist.UShortInput[joinMap.VolumeLevel.JoinNumber]);
- volumeDisplayWithFeedback.MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMute.JoinNumber]);
- volumeDisplayWithFeedback.MuteFeedback.LinkInputSig(trilist.BooleanInput[joinMap.VolumeMuteOn.JoinNumber]);
- volumeDisplayWithFeedback.MuteFeedback.LinkComplementInputSig(trilist.BooleanInput[joinMap.VolumeMuteOff.JoinNumber]);
- }
-
- }
-
- [Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")]
- public abstract class TwoWayDisplayBase : DisplayBase, IRoutingFeedback, IHasPowerControlWithFeedback
- {
- public StringFeedback CurrentInputFeedback { get; private set; }
-
- abstract protected Func CurrentInputFeedbackFunc { get; }
-
- public BoolFeedback PowerIsOnFeedback { get; protected set; }
-
- abstract protected Func PowerIsOnFeedbackFunc { get; }
-
-
- // public static MockDisplay DefaultDisplay
- // {
- // get
- // {
- // if (_DefaultDisplay == null)
- // _DefaultDisplay = new MockDisplay("default", "Default Display");
- // return _DefaultDisplay;
- // }
- //}
- //static MockDisplay _DefaultDisplay;
-
- public TwoWayDisplayBase(string key, string name)
- : base(key, name)
- {
- CurrentInputFeedback = new StringFeedback(CurrentInputFeedbackFunc);
-
- WarmupTime = 7000;
- CooldownTime = 15000;
-
- PowerIsOnFeedback = new BoolFeedback("PowerOnFeedback", PowerIsOnFeedbackFunc);
-
- Feedbacks.Add(CurrentInputFeedback);
- Feedbacks.Add(PowerIsOnFeedback);
-
- PowerIsOnFeedback.OutputChange += PowerIsOnFeedback_OutputChange;
-
- }
-
- void PowerIsOnFeedback_OutputChange(object sender, EventArgs e)
- {
- if (UsageTracker != null)
- {
- if (PowerIsOnFeedback.BoolValue)
- UsageTracker.StartDeviceUsage();
- else
- UsageTracker.EndDeviceUsage();
- }
- }
-
- public event EventHandler NumericSwitchChange;
-
- ///
- /// Raise an event when the status of a switch object changes.
- ///
- /// Arguments defined as IKeyName sender, output, input, and eRoutingSignalType
- protected void OnSwitchChange(RoutingNumericEventArgs e)
- {
- var newEvent = NumericSwitchChange;
- if (newEvent != null) newEvent(this, e);
- }
- }
-}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs
index 04f2490a..9a2667f0 100644
--- a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs
+++ b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs
@@ -9,6 +9,7 @@ using Crestron.SimplSharpPro.Fusion;
using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Essentials.Core.Config;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using Serilog.Events;
using System;
using System.Collections.Generic;
@@ -19,7 +20,7 @@ namespace PepperDash.Essentials.Core.Fusion
{
public class EssentialsHuddleSpaceFusionSystemControllerBase : Device, IOccupancyStatusProvider
{
- protected EssentialsHuddleSpaceRoomFusionRoomJoinMap JoinMap;
+ private readonly EssentialsHuddleSpaceRoomFusionRoomJoinMap JoinMap;
private const string RemoteOccupancyXml = "Local{0}";
private readonly bool _guidFileExists;
@@ -29,15 +30,15 @@ namespace PepperDash.Essentials.Core.Fusion
protected StringSigData CurrentRoomSourceNameSig;
- public FusionCustomPropertiesBridge CustomPropertiesBridge = new FusionCustomPropertiesBridge();
+ private readonly FusionCustomPropertiesBridge CustomPropertiesBridge = new FusionCustomPropertiesBridge();
protected FusionOccupancySensorAsset FusionOccSensor;
- protected FusionRemoteOccupancySensor FusionRemoteOccSensor;
+ private readonly FusionRemoteOccupancySensor FusionRemoteOccSensor;
protected FusionRoom FusionRoom;
protected Dictionary FusionStaticAssets;
- public long PushNotificationTimeout = 5000;
- protected IEssentialsRoom Room;
- public long SchedulePollInterval = 300000;
+ private readonly long PushNotificationTimeout = 5000;
+ private readonly IEssentialsRoom Room;
+ private readonly long SchedulePollInterval = 300000;
private Event _currentMeeting;
private RoomSchedule _currentSchedule;
@@ -85,7 +86,7 @@ namespace PepperDash.Essentials.Core.Fusion
#region Default Display Source Sigs
- private BooleanSigData[] _source = new BooleanSigData[10];
+ private readonly BooleanSigData[] _source = new BooleanSigData[10];
#endregion
@@ -151,9 +152,8 @@ namespace PepperDash.Essentials.Core.Fusion
ReadGuidFile(guidFilePath);
}
- var occupancyRoom = Room as IRoomOccupancy;
- if (occupancyRoom != null)
+ if (Room is IRoomOccupancy occupancyRoom)
{
if (occupancyRoom.RoomOccupancy != null)
{
@@ -367,8 +367,7 @@ namespace PepperDash.Essentials.Core.Fusion
CurrentRoomSourceNameSig = FusionRoom.CreateOffsetStringSig(JoinMap.Display1CurrentSourceName.JoinNumber, JoinMap.Display1CurrentSourceName.AttributeName,
eSigIoMask.InputSigOnly);
// Don't think we need to get current status of this as nothing should be alive yet.
- var hasCurrentSourceInfoChange = Room as IHasCurrentSourceInfoChange;
- if (hasCurrentSourceInfoChange != null)
+ if (Room is IHasCurrentSourceInfoChange hasCurrentSourceInfoChange)
{
hasCurrentSourceInfoChange.CurrentSourceChange += Room_CurrentSourceInfoChange;
}
@@ -377,8 +376,7 @@ namespace PepperDash.Essentials.Core.Fusion
FusionRoom.SystemPowerOn.OutputSig.SetSigFalseAction(Room.PowerOnToDefaultOrLastSource);
FusionRoom.SystemPowerOff.OutputSig.SetSigFalseAction(() =>
{
- var runRouteAction = Room as IRunRouteAction;
- if (runRouteAction != null)
+ if (Room is IRunRouteAction runRouteAction)
{
runRouteAction.RunRouteAction("roomOff", Room.SourceListKey);
}
@@ -660,7 +658,7 @@ namespace PepperDash.Essentials.Core.Fusion
var extendTime = _currentMeeting.dtEnd - DateTime.Now;
var extendMinutesRaw = extendTime.TotalMinutes;
- extendMinutes = extendMinutes + (int) Math.Round(extendMinutesRaw);
+ extendMinutes += (int) Math.Round(extendMinutesRaw);
}
@@ -901,12 +899,7 @@ namespace PepperDash.Essentials.Core.Fusion
}
}
}
-
- var handler = RoomInfoChange;
- if (handler != null)
- {
- handler(this, new EventArgs());
- }
+ RoomInfoChange?.Invoke(this, new EventArgs());
CustomPropertiesBridge.EvaluateRoomInfo(Room.Key, roomInformation);
}
@@ -1014,12 +1007,7 @@ namespace PepperDash.Essentials.Core.Fusion
}
// Fire Schedule Change Event
- var handler = ScheduleChange;
-
- if (handler != null)
- {
- handler(this, new ScheduleChangeEventArgs {Schedule = _currentSchedule});
- }
+ ScheduleChange?.Invoke(this, new ScheduleChangeEventArgs { Schedule = _currentSchedule });
}
}
}
@@ -1091,7 +1079,7 @@ namespace PepperDash.Essentials.Core.Fusion
}
}
- var laptops = dict.Where(d => d.Value.SourceDevice is Devices.Laptop);
+ var laptops = dict.Where(d => d.Value.SourceDevice is IRoutingSource);
i = 1;
foreach (var kvp in laptops)
{
@@ -1123,9 +1111,7 @@ namespace PepperDash.Essentials.Core.Fusion
///
protected void UsageTracker_DeviceUsageEnded(object sender, DeviceUsageEventArgs e)
{
- var deviceTracker = sender as UsageTracking;
-
- if (deviceTracker == null)
+ if (!(sender is UsageTracking deviceTracker))
{
return;
}
@@ -1168,8 +1154,7 @@ namespace PepperDash.Essentials.Core.Fusion
// And respond to selection in Fusion
sigD.OutputSig.SetSigFalseAction(() =>
{
- var runRouteAction = Room as IRunRouteAction;
- if (runRouteAction != null)
+ if (Room is IRunRouteAction runRouteAction)
{
runRouteAction.RunRouteAction(routeKey, Room.SourceListKey);
}
@@ -1213,12 +1198,11 @@ namespace PepperDash.Essentials.Core.Fusion
//uint attrNum = Convert.ToUInt32(keyNum);
// Check for UI devices
- var uiDev = dev as IHasBasicTriListWithSmartObject;
- if (uiDev != null)
+ if (dev is IHasBasicTriListWithSmartObject uiDev)
{
if (uiDev.Panel is Crestron.SimplSharpPro.UI.XpanelForSmartGraphics)
{
- attrNum = attrNum + touchpanelNum;
+ attrNum += touchpanelNum;
if (attrNum > JoinMap.XpanelOnlineStart.JoinSpan)
{
@@ -1231,7 +1215,7 @@ namespace PepperDash.Essentials.Core.Fusion
}
else
{
- attrNum = attrNum + xpanelNum;
+ attrNum += xpanelNum;
if (attrNum > JoinMap.TouchpanelOnlineStart.JoinSpan)
{
@@ -1245,9 +1229,9 @@ namespace PepperDash.Essentials.Core.Fusion
}
//else
- if (dev is DisplayBase)
+ if (dev is IDisplay)
{
- attrNum = attrNum + displayNum;
+ attrNum += displayNum;
if (attrNum > JoinMap.DisplayOnlineStart.JoinSpan)
{
continue;
@@ -1289,23 +1273,21 @@ namespace PepperDash.Essentials.Core.Fusion
{
//Setup Display Usage Monitoring
- var displays = DeviceManager.AllDevices.Where(d => d is DisplayBase);
+ var displays = DeviceManager.AllDevices.Where(d => d is IDisplay);
// Consider updating this in multiple display systems
- foreach (var display in displays.Cast())
+ foreach (var display in displays.Cast())
{
- display.UsageTracker = new UsageTracking(display) {UsageIsTracked = true};
+ display.UsageTracker = new UsageTracking(display as Device) {UsageIsTracked = true};
display.UsageTracker.DeviceUsageEnded += UsageTracker_DeviceUsageEnded;
}
- var hasDefaultDisplay = Room as IHasDefaultDisplay;
- if (hasDefaultDisplay == null)
+ if (!(Room is IHasDefaultDisplay hasDefaultDisplay))
{
return;
}
- var defaultDisplay = hasDefaultDisplay.DefaultDisplay as DisplayBase;
- if (defaultDisplay == null)
+ if (!(hasDefaultDisplay.DefaultDisplay is IDisplay defaultDisplay))
{
Debug.LogMessage(LogEventLevel.Debug, this, "Cannot link null display to Fusion because default display is null");
return;
@@ -1357,8 +1339,7 @@ namespace PepperDash.Essentials.Core.Fusion
dispAsset.PowerOn.OutputSig.UserObject = dispPowerOnAction;
dispAsset.PowerOff.OutputSig.UserObject = dispPowerOffAction;
- var defaultTwoWayDisplay = defaultDisplay as IHasPowerControlWithFeedback;
- if (defaultTwoWayDisplay != null)
+ if (defaultDisplay is IHasPowerControlWithFeedback defaultTwoWayDisplay)
{
defaultTwoWayDisplay.PowerIsOnFeedback.LinkInputSig(FusionRoom.DisplayPowerOn.InputSig);
if (defaultDisplay is IDisplayUsage)
@@ -1370,8 +1351,8 @@ namespace PepperDash.Essentials.Core.Fusion
}
// Use extension methods
- dispAsset.TrySetMakeModel(defaultDisplay);
- dispAsset.TryLinkAssetErrorToCommunication(defaultDisplay);
+ dispAsset.TrySetMakeModel(defaultDisplay as Device);
+ dispAsset.TryLinkAssetErrorToCommunication(defaultDisplay as Device);
}
catch (Exception e)
{
@@ -1386,13 +1367,12 @@ namespace PepperDash.Essentials.Core.Fusion
///
///
/// a
- protected virtual void MapDisplayToRoomJoins(int displayIndex, uint joinOffset, DisplayBase display)
+ protected virtual void MapDisplayToRoomJoins(int displayIndex, uint joinOffset, IDisplay display)
{
var displayName = string.Format("Display {0} - ", displayIndex);
- var hasDefaultDisplay = Room as IHasDefaultDisplay;
- if (hasDefaultDisplay == null || display != hasDefaultDisplay.DefaultDisplay)
+ if (!(Room is IHasDefaultDisplay hasDefaultDisplay) || display != hasDefaultDisplay.DefaultDisplay)
{
return;
}
@@ -1401,8 +1381,7 @@ namespace PepperDash.Essentials.Core.Fusion
eSigIoMask.InputOutputSig);
defaultDisplayVolume.OutputSig.UserObject = new Action(b =>
{
- var basicVolumeWithFeedback = display as IBasicVolumeWithFeedback;
- if (basicVolumeWithFeedback == null)
+ if (!(display is IBasicVolumeWithFeedback basicVolumeWithFeedback))
{
return;
}
@@ -1435,8 +1414,7 @@ namespace PepperDash.Essentials.Core.Fusion
});
- var defaultTwoWayDisplay = display as IHasPowerControlWithFeedback;
- if (defaultTwoWayDisplay != null)
+ if (display is IHasPowerControlWithFeedback defaultTwoWayDisplay)
{
defaultTwoWayDisplay.PowerIsOnFeedback.LinkInputSig(defaultDisplayPowerOn.InputSig);
defaultTwoWayDisplay.PowerIsOnFeedback.LinkComplementInputSig(defaultDisplayPowerOff.InputSig);
@@ -1449,8 +1427,7 @@ namespace PepperDash.Essentials.Core.Fusion
{
if (!b)
{
- var runRouteAction = Room as IRunRouteAction;
- if (runRouteAction != null)
+ if (Room is IRunRouteAction runRouteAction)
{
runRouteAction.RunRouteAction("roomOff", Room.SourceListKey);
}
@@ -1464,8 +1441,7 @@ namespace PepperDash.Essentials.Core.Fusion
_errorMessageRollUp = new StatusMonitorCollection(this);
foreach (var dev in DeviceManager.GetDevices())
{
- var md = dev as ICommunicationMonitor;
- if (md != null)
+ if (dev is ICommunicationMonitor md)
{
_errorMessageRollUp.AddMonitor(md.CommunicationMonitor);
Debug.LogMessage(LogEventLevel.Verbose, this, "Adding '{0}' to room's overall error monitor",
@@ -1531,9 +1507,8 @@ namespace PepperDash.Essentials.Core.Fusion
// Tie to method on occupancy object
//occSensorShutdownMinutes.OutputSig.UserObject(new Action(ushort)(b => Room.OccupancyObj.SetShutdownMinutes(b));
- var occRoom = Room as IRoomOccupancy;
- if (occRoom != null)
+ if (Room is IRoomOccupancy occRoom)
{
occRoom.RoomOccupancy.RoomIsOccupiedFeedback.LinkInputSig(occSensorAsset.RoomOccupied.InputSig);
occRoom.RoomOccupancy.RoomIsOccupiedFeedback.OutputChange += RoomIsOccupiedFeedback_OutputChange;
@@ -1600,10 +1575,9 @@ namespace PepperDash.Essentials.Core.Fusion
// The sig/UO method: Need separate handlers for fixed and user sigs, all flavors,
// even though they all contain sigs.
- var sigData = args.UserConfiguredSigDetail as BooleanSigDataFixedName;
BoolOutputSig outSig;
- if (sigData != null)
+ if (args.UserConfiguredSigDetail is BooleanSigDataFixedName sigData)
{
outSig = sigData.OutputSig;
if (outSig.UserObject is Action)
@@ -1759,8 +1733,7 @@ namespace PepperDash.Essentials.Core.Fusion
///
public static void TrySetMakeModel(this FusionStaticAsset asset, Device device)
{
- var mm = device as IMakeModel;
- if (mm != null)
+ if (device is IMakeModel mm)
{
asset.ParamMake.Value = mm.DeviceMake;
asset.ParamModel.Value = mm.DeviceModel;
diff --git a/src/PepperDash.Essentials.Core/Lighting/LightingBase.cs b/src/PepperDash.Essentials.Core/Lighting/LightingBase.cs
deleted file mode 100644
index b9978b7c..00000000
--- a/src/PepperDash.Essentials.Core/Lighting/LightingBase.cs
+++ /dev/null
@@ -1,171 +0,0 @@
-
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using Crestron.SimplSharp;
-using Crestron.SimplSharpPro;
-using Crestron.SimplSharpPro.DeviceSupport;
-using Newtonsoft.Json;
-using PepperDash.Core;
-using PepperDash.Essentials.Core.Bridges;
-using Serilog.Events;
-
-namespace PepperDash.Essentials.Core.Lighting
-{
- [Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")]
- public abstract class LightingBase : EssentialsBridgeableDevice, ILightingScenes
- {
- #region ILightingScenes Members
-
- public event EventHandler LightingSceneChange;
-
- public List LightingScenes { get; protected set; }
-
- public LightingScene CurrentLightingScene { get; protected set; }
-
- public IntFeedback CurrentLightingSceneFeedback { get; protected set; }
-
- #endregion
-
- protected LightingBase(string key, string name)
- : base(key, name)
- {
- LightingScenes = new List();
-
- CurrentLightingScene = new LightingScene();
- //CurrentLightingSceneFeedback = new IntFeedback(() => { return int.Parse(this.CurrentLightingScene.ID); });
- }
-
- public abstract void SelectScene(LightingScene scene);
-
- public void SimulateSceneSelect(string sceneName)
- {
- Debug.LogMessage(LogEventLevel.Debug, this, "Simulating selection of scene '{0}'", sceneName);
-
- var scene = LightingScenes.FirstOrDefault(s => s.Name.Equals(sceneName));
-
- if (scene != null)
- {
- CurrentLightingScene = scene;
- OnLightingSceneChange();
- }
- }
-
- ///
- /// Sets the IsActive property on each scene and fires the LightingSceneChange event
- ///
- protected void OnLightingSceneChange()
- {
- foreach (var scene in LightingScenes)
- {
- if (scene == CurrentLightingScene)
- scene.IsActive = true;
-
- else
- scene.IsActive = false;
- }
-
- var handler = LightingSceneChange;
- if (handler != null)
- {
- handler(this, new LightingSceneChangeEventArgs(CurrentLightingScene));
- }
- }
-
- protected GenericLightingJoinMap LinkLightingToApi(LightingBase lightingDevice, BasicTriList trilist, uint joinStart,
- string joinMapKey, EiscApiAdvanced bridge)
- {
- var joinMap = new GenericLightingJoinMap(joinStart);
-
- var joinMapSerialized = JoinMapHelper.GetSerializedJoinMapForDevice(joinMapKey);
-
- if (!string.IsNullOrEmpty(joinMapSerialized))
- joinMap = JsonConvert.DeserializeObject(joinMapSerialized);
-
- if (bridge != null)
- {
- bridge.AddJoinMap(Key, joinMap);
- }
- else
- {
- Debug.LogMessage(LogEventLevel.Information, this, "Please update config to use 'eiscapiadvanced' to get all join map features for this device.");
- }
-
- return LinkLightingToApi(lightingDevice, trilist, joinMap);
- }
-
- protected GenericLightingJoinMap LinkLightingToApi(LightingBase lightingDevice, BasicTriList trilist, GenericLightingJoinMap joinMap)
- {
- Debug.LogMessage(LogEventLevel.Debug, "Linking to Trilist '{0}'", trilist.ID.ToString("X"));
-
- Debug.LogMessage(LogEventLevel.Information, "Linking to Lighting Type {0}", lightingDevice.GetType().Name.ToString());
-
- // GenericLighitng Actions & FeedBack
- trilist.SetUShortSigAction(joinMap.SelectScene.JoinNumber, u => lightingDevice.SelectScene(lightingDevice.LightingScenes[u]));
-
- var sceneIndex = 0;
- foreach (var scene in lightingDevice.LightingScenes)
- {
- var index = sceneIndex;
-
- trilist.SetSigTrueAction((uint)(joinMap.SelectSceneDirect.JoinNumber + index), () => lightingDevice.SelectScene(lightingDevice.LightingScenes[index]));
- scene.IsActiveFeedback.LinkInputSig(trilist.BooleanInput[(uint)(joinMap.SelectSceneDirect.JoinNumber + index)]);
- trilist.StringInput[(uint)(joinMap.SelectSceneDirect.JoinNumber + index)].StringValue = scene.Name;
- trilist.BooleanInput[(uint)(joinMap.ButtonVisibility.JoinNumber + index)].BoolValue = true;
-
- sceneIndex++;
- }
-
- trilist.OnlineStatusChange += (sender, args) =>
- {
- if (!args.DeviceOnLine) return;
-
- sceneIndex = 0;
- foreach (var scene in lightingDevice.LightingScenes)
- {
- var index = sceneIndex;
-
- trilist.StringInput[(uint) (joinMap.SelectSceneDirect.JoinNumber + index)].StringValue = scene.Name;
- trilist.BooleanInput[(uint) (joinMap.ButtonVisibility.JoinNumber + index)].BoolValue = true;
- scene.IsActiveFeedback.FireUpdate();
-
- sceneIndex++;
- }
- };
-
- return joinMap;
- }
- }
-
- public class LightingScene
- {
- [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
- public string Name { get; set; }
- [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)]
- public string ID { get; set; }
- bool _IsActive;
- [JsonProperty("isActive", NullValueHandling = NullValueHandling.Ignore)]
- public bool IsActive
- {
- get
- {
- return _IsActive;
- }
- set
- {
- _IsActive = value;
- IsActiveFeedback.FireUpdate();
- }
- }
-
- [JsonIgnore]
- public BoolFeedback IsActiveFeedback { get; set; }
-
- public LightingScene()
- {
- IsActiveFeedback = new BoolFeedback(new Func(() => IsActive));
- }
- }
-}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Lighting/LightingScene.cs b/src/PepperDash.Essentials.Core/Lighting/LightingScene.cs
new file mode 100644
index 00000000..772a58b7
--- /dev/null
+++ b/src/PepperDash.Essentials.Core/Lighting/LightingScene.cs
@@ -0,0 +1,37 @@
+
+
+using System;
+using Newtonsoft.Json;
+
+namespace PepperDash.Essentials.Core.Lighting
+{
+ public class LightingScene
+ {
+ [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
+ public string Name { get; set; }
+ [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)]
+ public string ID { get; set; }
+ bool _IsActive;
+ [JsonProperty("isActive", NullValueHandling = NullValueHandling.Ignore)]
+ public bool IsActive
+ {
+ get
+ {
+ return _IsActive;
+ }
+ set
+ {
+ _IsActive = value;
+ IsActiveFeedback.FireUpdate();
+ }
+ }
+
+ [JsonIgnore]
+ public BoolFeedback IsActiveFeedback { get; set; }
+
+ public LightingScene()
+ {
+ IsActiveFeedback = new BoolFeedback(new Func(() => IsActive));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Monitoring/GenericCommunicationMonitor.cs b/src/PepperDash.Essentials.Core/Monitoring/GenericCommunicationMonitor.cs
index aad8120d..094cdb22 100644
--- a/src/PepperDash.Essentials.Core/Monitoring/GenericCommunicationMonitor.cs
+++ b/src/PepperDash.Essentials.Core/Monitoring/GenericCommunicationMonitor.cs
@@ -151,17 +151,16 @@ namespace PepperDash.Essentials.Core
{
if (MonitorBytesReceived)
{
- Client.BytesReceived += Client_BytesReceived;
+ Client.BytesReceived -= Client_BytesReceived;
+ Client.BytesReceived += Client_BytesReceived;
}
else
{
+ Client.TextReceived -= Client_TextReceived;
Client.TextReceived += Client_TextReceived;
}
- if (!IsSocket)
- {
- BeginPolling();
- }
+ BeginPolling();
}
void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e)
diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj
index 778d471e..b1406d69 100644
--- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj
+++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj
@@ -1,11 +1,11 @@
- ProgramLibrary
Debug;Release;Debug 4.7.2
net472
true
+ true
bin\$(Configuration)\
PepperDash_Essentials_Core
PepperDash.Essentials.Core
@@ -13,7 +13,6 @@
PepperDash.Essentials.Core
$(Version)
false
- 2.0.0-local
full
@@ -26,9 +25,14 @@
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs b/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs
index 8ea9772f..8649d292 100644
--- a/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs
+++ b/src/PepperDash.Essentials.Core/Touchpanels/Mpc3Touchpanel.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using Crestron.SimplSharpPro;
using Newtonsoft.Json;
using PepperDash.Core;
+using PepperDash.Core.Logging;
using Serilog.Events;
namespace PepperDash.Essentials.Core.Touchpanels
@@ -34,9 +35,9 @@ namespace PepperDash.Essentials.Core.Touchpanels
Debug.LogMessage(LogEventLevel.Information, this, "touchpanel registration response: {0}", registrationResponse);
}
- _touchpanel.BaseEvent += _touchpanel_BaseEvent;
- _touchpanel.ButtonStateChange += _touchpanel_ButtonStateChange;
- _touchpanel.PanelStateChange += _touchpanel_PanelStateChange;
+ _touchpanel.BaseEvent += Touchpanel_BaseEvent;
+ _touchpanel.ButtonStateChange += Touchpanel_ButtonStateChange;
+ _touchpanel.PanelStateChange += Touchpanel_PanelStateChange;
_buttons = buttons;
if (_buttons == null)
@@ -74,10 +75,9 @@ namespace PepperDash.Essentials.Core.Touchpanels
return;
}
- int buttonNumber;
- TryParseInt(key, out buttonNumber);
+ TryParseInt(key, out int buttonNumber);
- var buttonEventTypes = config.EventTypes;
+ var buttonEventTypes = config.EventTypes;
BoolOutputSig enabledFb = null;
BoolOutputSig disabledFb = null;
@@ -161,11 +161,10 @@ namespace PepperDash.Essentials.Core.Touchpanels
return;
}
- int buttonNumber;
- TryParseInt(key, out buttonNumber);
+ TryParseInt(key, out int buttonNumber);
- // Link up the button feedbacks to the specified device feedback
- var buttonFeedback = config.Feedback;
+ // Link up the button feedbacks to the specified device feedback
+ var buttonFeedback = config.Feedback;
if (buttonFeedback == null || string.IsNullOrEmpty(buttonFeedback.DeviceKey))
{
Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedback not configured, skipping.",
@@ -177,15 +176,14 @@ namespace PepperDash.Essentials.Core.Touchpanels
try
{
- var device = DeviceManager.GetDeviceForKey(buttonFeedback.DeviceKey) as Device;
- if (device == null)
- {
- Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedback deviceKey '{1}' not found.",
- key, buttonFeedback.DeviceKey);
- return;
- }
+ if (!(DeviceManager.GetDeviceForKey(buttonFeedback.DeviceKey) is Device device))
+ {
+ Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedback deviceKey '{1}' not found.",
+ key, buttonFeedback.DeviceKey);
+ return;
+ }
- deviceFeedback = device.GetFeedbackProperty(buttonFeedback.FeedbackName);
+ deviceFeedback = device.GetFeedbackProperty(buttonFeedback.FeedbackName);
if (deviceFeedback == null)
{
Debug.LogMessage(LogEventLevel.Debug, this, "Button '{0}' feedbackName property '{1}' not found.",
@@ -224,38 +222,37 @@ namespace PepperDash.Essentials.Core.Touchpanels
}
var boolFeedback = deviceFeedback as BoolFeedback;
- var intFeedback = deviceFeedback as IntFeedback;
- switch (key)
- {
- case ("power"):
- {
- if (boolFeedback != null) boolFeedback.LinkCrestronFeedback(_touchpanel.FeedbackPower);
- break;
- }
- case ("volumeup"):
- case ("volumedown"):
- case ("volumefeedback"):
- {
- if (intFeedback != null)
- {
- var volumeFeedback = intFeedback;
- volumeFeedback.LinkInputSig(_touchpanel.VolumeBargraph);
- }
- break;
- }
- case ("mute"):
- {
- if (boolFeedback != null) boolFeedback.LinkCrestronFeedback(_touchpanel.FeedbackMute);
- break;
- }
- default:
- {
- if (boolFeedback != null) boolFeedback.LinkCrestronFeedback(_touchpanel.Feedbacks[(uint)buttonNumber]);
- break;
- }
- }
- }
+ switch (key)
+ {
+ case ("power"):
+ {
+ boolFeedback?.LinkCrestronFeedback(_touchpanel.FeedbackPower);
+ break;
+ }
+ case ("volumeup"):
+ case ("volumedown"):
+ case ("volumefeedback"):
+ {
+ if (deviceFeedback is IntFeedback intFeedback)
+ {
+ var volumeFeedback = intFeedback;
+ volumeFeedback.LinkInputSig(_touchpanel.VolumeBargraph);
+ }
+ break;
+ }
+ case ("mute"):
+ {
+ boolFeedback?.LinkCrestronFeedback(_touchpanel.FeedbackMute);
+ break;
+ }
+ default:
+ {
+ boolFeedback?.LinkCrestronFeedback(_touchpanel.Feedbacks[(uint)buttonNumber]);
+ break;
+ }
+ }
+ }
///
/// Try parse int helper method
@@ -277,12 +274,12 @@ namespace PepperDash.Essentials.Core.Touchpanels
}
}
- private void _touchpanel_BaseEvent(GenericBase device, BaseEventArgs args)
+ private void Touchpanel_BaseEvent(GenericBase device, BaseEventArgs args)
{
Debug.LogMessage(LogEventLevel.Debug, this, "BaseEvent: eventId-'{0}', index-'{1}'", args.EventId, args.Index);
}
- private void _touchpanel_ButtonStateChange(GenericBase device, Crestron.SimplSharpPro.DeviceSupport.ButtonEventArgs args)
+ private void Touchpanel_ButtonStateChange(GenericBase device, Crestron.SimplSharpPro.DeviceSupport.ButtonEventArgs args)
{
Debug.LogMessage(LogEventLevel.Debug, this, "ButtonStateChange: buttonNumber-'{0}' buttonName-'{1}', buttonState-'{2}'", args.Button.Number, args.Button.Name, args.NewButtonState);
var type = args.NewButtonState.ToString();
@@ -297,7 +294,7 @@ namespace PepperDash.Essentials.Core.Touchpanels
}
}
- private void _touchpanel_PanelStateChange(GenericBase device, BaseEventArgs args)
+ private void Touchpanel_PanelStateChange(GenericBase device, BaseEventArgs args)
{
Debug.LogMessage(LogEventLevel.Debug, this, "PanelStateChange: eventId-'{0}', index-'{1}'", args.EventId, args.Index);
}
@@ -310,7 +307,7 @@ namespace PepperDash.Essentials.Core.Touchpanels
///
public void Press(string buttonKey, string type)
{
- Debug.LogMessage(LogEventLevel.Verbose, this, "Press: buttonKey-'{0}', type-'{1}'", buttonKey, type);
+ this.LogVerbose("Press: buttonKey-'{buttonKey}', type-'{type}'", buttonKey, type);
// TODO: In future, consider modifying this to generate actions at device activation time
// to prevent the need to dynamically call the method via reflection on each button press
@@ -325,18 +322,12 @@ namespace PepperDash.Essentials.Core.Touchpanels
public void ListButtons()
{
- var line = new string('-', 35);
-
- Debug.Console(0, this, line);
-
- Debug.Console(0, this, "MPC3 Controller {0} - Available Butons", Key);
+ this.LogVerbose("MPC3 Controller {0} - Available Buttons", Key);
foreach (var button in _buttons)
{
- Debug.Console(0, this, "Key: {0}", button.Key);
+ this.LogVerbose("Key: {key}", button.Key);
}
-
- Debug.Console(0, this, line);
}
}
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs
index 6378f1b2..fa20145c 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/SetDeviceStreamDebugRequestHandler.cs
@@ -119,23 +119,23 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
return;
}
- var device = DeviceManager.GetDeviceForKey(body.DeviceKey) as IStreamDebugging;
- if (device == null)
- {
- context.Response.StatusCode = 404;
- context.Response.StatusDescription = "Not Found";
- context.Response.End();
+ if (!(DeviceManager.GetDeviceForKey(body.DeviceKey) is IStreamDebugging device))
+ {
+ context.Response.StatusCode = 404;
+ context.Response.StatusDescription = "Not Found";
+ context.Response.End();
- return;
- }
-
- eStreamDebuggingSetting debugSetting;
+ return;
+ }
+
+ eStreamDebuggingSetting debugSetting;
try
{
debugSetting = (eStreamDebuggingSetting) Enum.Parse(typeof (eStreamDebuggingSetting), body.Setting, true);
}
catch (Exception ex)
{
+ Debug.LogMessage(ex, "Exception handling set debug request");
context.Response.StatusCode = 500;
context.Response.StatusDescription = "Internal Server Error";
context.Response.End();
@@ -161,6 +161,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
}
catch (Exception ex)
{
+ Debug.LogMessage(ex, "Exception handling set debug request");
context.Response.StatusCode = 500;
context.Response.StatusDescription = "Internal Server Error";
context.Response.End();
diff --git a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs
index fa82ba1f..384d60a3 100644
--- a/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs
+++ b/src/PepperDash.Essentials.Devices.Common/Cameras/CameraVisca.cs
@@ -21,7 +21,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
{
public class CameraVisca : CameraBase, IHasCameraPtzControl, ICommunicationMonitor, IHasCameraPresets, IHasPowerControlWithFeedback, IBridgeAdvanced, IHasCameraFocusControl, IHasAutoFocusMode
{
- CameraViscaPropertiesConfig PropertiesConfig;
+ private readonly CameraViscaPropertiesConfig PropertiesConfig;
public IBasicCommunication Communication { get; private set; }
@@ -30,7 +30,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
///
/// Used to store the actions to parse inquiry responses as the inquiries are sent
///
- private CrestronQueue> InquiryResponseQueue;
+ private readonly CrestronQueue> InquiryResponseQueue;
///
/// Camera ID (Default 1)
@@ -45,7 +45,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
public byte PanSpeedFast = 0x13;
public byte TiltSpeedFast = 0x13;
- private bool IsMoving;
+ // private bool IsMoving;
private bool IsZooming;
bool _powerIsOn;
@@ -101,18 +101,17 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
Capabilities = eCameraCapabilities.Pan | eCameraCapabilities.Tilt | eCameraCapabilities.Zoom | eCameraCapabilities.Focus;
Communication = comm;
- var socket = comm as ISocketStatus;
- if (socket != null)
- {
- // This instance uses IP control
- socket.ConnectionChange += new EventHandler(socket_ConnectionChange);
- }
- else
- {
- // This instance uses RS-232 control
- }
+ if (comm is ISocketStatus socket)
+ {
+ // This instance uses IP control
+ socket.ConnectionChange += new EventHandler(Socket_ConnectionChange);
+ }
+ else
+ {
+ // This instance uses RS-232 control
+ }
- Communication.BytesReceived += new EventHandler(Communication_BytesReceived);
+ Communication.BytesReceived += new EventHandler(Communication_BytesReceived);
PowerIsOnFeedback = new BoolFeedback(() => { return PowerIsOn; });
CameraIsOffFeedback = new BoolFeedback(() => { return !PowerIsOn; });
@@ -175,7 +174,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
LinkCameraToApi(this, trilist, joinStart, joinMapKey, bridge);
}
- void socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e)
+ void Socket_ConnectionChange(object sender, GenericSocketStatusChageEventArgs e)
{
Debug.LogMessage(LogEventLevel.Verbose, this, "Socket Status Change: {0}", e.Client.ClientStatus.ToString());
@@ -449,12 +448,12 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
public void PanLeft()
{
SendPanTiltCommand(new byte[] {0x01, 0x03}, false);
- IsMoving = true;
+ // IsMoving = true;
}
public void PanRight()
{
SendPanTiltCommand(new byte[] { 0x02, 0x03 }, false);
- IsMoving = true;
+ // IsMoving = true;
}
public void PanStop()
{
@@ -463,12 +462,12 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
public void TiltDown()
{
SendPanTiltCommand(new byte[] { 0x03, 0x02 }, false);
- IsMoving = true;
+ // IsMoving = true;
}
public void TiltUp()
{
SendPanTiltCommand(new byte[] { 0x03, 0x01 }, false);
- IsMoving = true;
+ // IsMoving = true;
}
public void TiltStop()
{
@@ -507,7 +506,7 @@ namespace PepperDash.Essentials.Devices.Common.Cameras
{
StopSpeedTimer();
SendPanTiltCommand(new byte[] { 0x03, 0x03 }, false);
- IsMoving = false;
+ // IsMoving = false;
}
}
public void PositionHome()
diff --git a/src/PepperDash.Essentials.Devices.Common/Codec/iHasScheduleAwareness.cs b/src/PepperDash.Essentials.Devices.Common/Codec/iHasScheduleAwareness.cs
index 6d8d4a4a..602d65e3 100644
--- a/src/PepperDash.Essentials.Devices.Common/Codec/iHasScheduleAwareness.cs
+++ b/src/PepperDash.Essentials.Devices.Common/Codec/iHasScheduleAwareness.cs
@@ -40,9 +40,9 @@ namespace PepperDash.Essentials.Devices.Common.Codec
private int _meetingWarningMinutes = 5;
- private Meeting _previousChangedMeeting;
+ //private Meeting _previousChangedMeeting;
- private eMeetingEventChangeType _previousChangeType = eMeetingEventChangeType.Unknown;
+ //private eMeetingEventChangeType _previousChangeType = eMeetingEventChangeType.Unknown;
public int MeetingWarningMinutes
{
@@ -62,16 +62,11 @@ namespace PepperDash.Essentials.Devices.Common.Codec
set
{
_meetings = value;
-
- var handler = MeetingsListHasChanged;
- if (handler != null)
- {
- handler(this, new EventArgs());
- }
+ MeetingsListHasChanged?.Invoke(this, new EventArgs());
}
}
- private CTimer _scheduleChecker;
+ private readonly CTimer _scheduleChecker;
public CodecScheduleAwareness()
{
@@ -99,12 +94,7 @@ namespace PepperDash.Essentials.Devices.Common.Codec
{
// Add this change type to the NotifiedChangeTypes
meeting.NotifiedChangeTypes |= changeType;
-
- var handler = MeetingEventChange;
- if (handler != null)
- {
- handler(this, new MeetingEventArgs() { ChangeType = changeType, Meeting = meeting });
- }
+ MeetingEventChange?.Invoke(this, new MeetingEventArgs() { ChangeType = changeType, Meeting = meeting });
}
else
{
diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs
index 9796599b..24d55c2e 100644
--- a/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs
+++ b/src/PepperDash.Essentials.Devices.Common/Displays/DisplayBase.cs
@@ -4,6 +4,7 @@ using Newtonsoft.Json;
using PepperDash.Core;
using PepperDash.Essentials.Core;
using PepperDash.Essentials.Core.Bridges;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
using Serilog.Events;
using System;
using System.Collections.Generic;
@@ -12,12 +13,7 @@ using Feedback = PepperDash.Essentials.Core.Feedback;
namespace PepperDash.Essentials.Devices.Common.Displays
{
- public abstract class DisplayBase : EssentialsDevice
- , IHasFeedback
- , IRoutingSinkWithSwitching
- , IHasPowerControl
- , IWarmingCooling
- , IUsageTracking
+ public abstract class DisplayBase : EssentialsDevice, IDisplay
{
private RoutingInputPort _currentInputPort;
public RoutingInputPort CurrentInputPort
diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs
index 0fecadc5..f972c1e6 100644
--- a/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs
+++ b/src/PepperDash.Essentials.Devices.Common/Displays/MockDisplay.cs
@@ -12,7 +12,7 @@ using Serilog.Events;
namespace PepperDash.Essentials.Devices.Common.Displays
{
- public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs, IRoutingSinkWithSwitchingWithInputPort, IHasPowerControlWithFeedback
+ public class MockDisplay : TwoWayDisplayBase, IBasicVolumeWithFeedback, IBridgeAdvanced, IHasInputs, IRoutingSinkWithSwitchingWithInputPort, IHasPowerControlWithFeedback
{
public ISelectableItems Inputs { get; private set; }
diff --git a/src/PepperDash.Essentials.Devices.Common/Lighting/LightingBase.cs b/src/PepperDash.Essentials.Devices.Common/Lighting/LightingBase.cs
index 1c67d426..6e1dc506 100644
--- a/src/PepperDash.Essentials.Devices.Common/Lighting/LightingBase.cs
+++ b/src/PepperDash.Essentials.Devices.Common/Lighting/LightingBase.cs
@@ -16,7 +16,6 @@ using Serilog.Events;
namespace PepperDash.Essentials.Devices.Common.Lighting
{
- [Obsolete("Please use PepperDash.Essentials.Devices.Common, this will be removed in 2.1")]
public abstract class LightingBase : EssentialsBridgeableDevice, ILightingScenes
{
#region ILightingScenes Members
@@ -68,12 +67,7 @@ namespace PepperDash.Essentials.Devices.Common.Lighting
else
scene.IsActive = false;
}
-
- var handler = LightingSceneChange;
- if (handler != null)
- {
- handler(this, new LightingSceneChangeEventArgs(CurrentLightingScene));
- }
+ LightingSceneChange?.Invoke(this, new LightingSceneChangeEventArgs(CurrentLightingScene));
}
protected GenericLightingJoinMap LinkLightingToApi(LightingBase lightingDevice, BasicTriList trilist, uint joinStart,
diff --git a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj
index a58861a5..49c05762 100644
--- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj
+++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj
@@ -1,6 +1,5 @@
- ProgramLibrary
Debug;Release;Debug 4.7.2
@@ -9,10 +8,9 @@
bin\$(Configuration)\
Essentials Devices Common
PepperDash.Essentials.Devices.Common
- True
+ true
PepperDash Essentials Devices Common
PepperDash.Essentials.Devices.Common
- 2.0.0-local
$(Version)
false
@@ -26,10 +24,10 @@
pdbonly
+
-
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs
index be781486..236c7ad9 100644
--- a/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs
+++ b/src/PepperDash.Essentials.Devices.Common/VideoCodec/VideoCodecBase.cs
@@ -31,9 +31,9 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
protected const int MaxParticipants = 50;
private readonly byte[] _clearBytes = XSigHelpers.ClearOutputs();
- private IHasDirectory _directoryCodec;
- private BasicTriList _directoryTrilist;
- private VideoCodecControllerJoinMap _directoryJoinmap;
+ private readonly IHasDirectory _directoryCodec;
+ private readonly BasicTriList _directoryTrilist;
+ private readonly VideoCodecControllerJoinMap _directoryJoinmap;
protected string _timeFormatSpecifier;
protected string _dateFormatSpecifier;
@@ -216,11 +216,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
///
protected virtual void OnCallStatusChange(CodecActiveCallItem item)
{
- var handler = CallStatusChange;
- if (handler != null)
- {
- handler(this, new CodecCallStatusItemChangeEventArgs(item));
- }
+ CallStatusChange?.Invoke(this, new CodecCallStatusItemChangeEventArgs(item));
PrivacyModeIsOnFeedback.FireUpdate();
@@ -252,12 +248,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
try
{
IsReady = true;
- var h = IsReadyChange;
- if (h != null)
- {
- h(this, new EventArgs());
- }
- }
+ IsReadyChange?.Invoke(this, new EventArgs());
+ }
catch (Exception e)
{
Debug.LogMessage(LogEventLevel.Verbose, this, "Error in SetIsReady() : {0}", e);
@@ -309,10 +301,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
joinMap.SetCustomJoinData(customJoins);
}
- if (bridge != null)
- {
- bridge.AddJoinMap(Key, joinMap);
- }
+ bridge?.AddJoinMap(Key, joinMap);
LinkVideoCodecToApi(codec, trilist, joinMap);
@@ -530,11 +519,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false);
- var autoCodec = codec as IHasCameraAutoMode;
- if (autoCodec == null) return;
+ if (!(codec is IHasCameraAutoMode autoCodec)) return;
- trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, autoCodec.CameraAutoModeIsOnFeedback.BoolValue);
+ trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, autoCodec.CameraAutoModeIsOnFeedback.BoolValue);
trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !autoCodec.CameraAutoModeIsOnFeedback.BoolValue);
};
@@ -548,11 +536,10 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false);
- var autoModeCodec = codec as IHasCameraAutoMode;
- if (autoModeCodec == null) return;
+ if (!(codec is IHasCameraAutoMode autoModeCodec)) return;
- trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, autoModeCodec.CameraAutoModeIsOnFeedback.BoolValue);
+ trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, autoModeCodec.CameraAutoModeIsOnFeedback.BoolValue);
trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !autoModeCodec.CameraAutoModeIsOnFeedback.BoolValue);
}
@@ -649,8 +636,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
var p = participant;
if (index > MaxParticipants) break;
- var audioMuteCodec = this as IHasParticipantAudioMute;
- if (audioMuteCodec != null)
+ if (this is IHasParticipantAudioMute audioMuteCodec)
{
trilist.SetSigFalseAction(joinMap.ParticipantAudioMuteToggleStart.JoinNumber + index,
() => audioMuteCodec.ToggleAudioForParticipant(p.UserId));
@@ -659,8 +645,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
() => audioMuteCodec.ToggleVideoForParticipant(p.UserId));
}
- var pinCodec = this as IHasParticipantPinUnpin;
- if (pinCodec != null)
+ if (this is IHasParticipantPinUnpin pinCodec)
{
trilist.SetSigFalseAction(joinMap.ParticipantPinToggleStart.JoinNumber + index,
() => pinCodec.ToggleParticipantPinState(p.UserId, pinCodec.ScreenIndexToPinUserTo));
@@ -1089,29 +1074,25 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
// Allow auto dial of selected line. Always dials first contact method
if (!trilist.GetBool(joinMap.DirectoryDisableAutoDialSelectedLine.JoinNumber))
{
- var invitableEntry = _selectedDirectoryItem as IInvitableContact;
-
- if (invitableEntry != null)
+ if (_selectedDirectoryItem is IInvitableContact invitableEntry)
{
Dial(invitableEntry);
return;
}
- var entryToDial = _selectedDirectoryItem as DirectoryContact;
- trilist.SetString(joinMap.DirectoryEntrySelectedNumber.JoinNumber,
+ trilist.SetString(joinMap.DirectoryEntrySelectedNumber.JoinNumber,
selectedContact != null ? selectedContact.ContactMethods[0].Number : string.Empty);
- if (entryToDial == null) return;
+ if (!(_selectedDirectoryItem is DirectoryContact entryToDial)) return;
Dial(entryToDial.ContactMethods[0].Number);
}
else
{
// If auto dial is disabled...
- var entryToDial = _selectedDirectoryItem as DirectoryContact;
- if (entryToDial == null)
+ if (!(_selectedDirectoryItem is DirectoryContact entryToDial))
{
// Clear out values and actions from last selected item
trilist.SetUshort(joinMap.SelectedContactMethodCount.JoinNumber, 0);
@@ -1296,78 +1277,76 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
trilist.SetUshort(joinMap.ConnectedCallCount.JoinNumber, (ushort)ActiveCalls.Count);
};
- var joinCodec = this as IJoinCalls;
- if (joinCodec != null)
- {
- trilist.SetSigFalseAction(joinMap.JoinAllCalls.JoinNumber, () => joinCodec.JoinAllCalls());
+ if (this is IJoinCalls joinCodec)
+ {
+ trilist.SetSigFalseAction(joinMap.JoinAllCalls.JoinNumber, () => joinCodec.JoinAllCalls());
- for (int i = 0; i < joinMap.JoinCallStart.JoinSpan; i++)
- {
- trilist.SetSigFalseAction((uint)(joinMap.JoinCallStart.JoinNumber + i), () =>
- {
- var call = ActiveCalls[i];
- if (call != null)
- {
- joinCodec.JoinCall(call);
- }
- else
- {
- Debug.LogMessage(LogEventLevel.Information, this, "[Join Call] Unable to find call at index '{0}'", i);
- }
- });
- }
- }
+ for (int i = 0; i < joinMap.JoinCallStart.JoinSpan; i++)
+ {
+ trilist.SetSigFalseAction((uint)(joinMap.JoinCallStart.JoinNumber + i), () =>
+ {
+ var call = ActiveCalls[i];
+ if (call != null)
+ {
+ joinCodec.JoinCall(call);
+ }
+ else
+ {
+ Debug.LogMessage(LogEventLevel.Information, this, "[Join Call] Unable to find call at index '{0}'", i);
+ }
+ });
+ }
+ }
- var holdCodec = this as IHasCallHold;
- if (holdCodec != null)
- {
- trilist.SetSigFalseAction(joinMap.HoldAllCalls.JoinNumber, () =>
- {
- foreach (var call in ActiveCalls)
- {
- holdCodec.HoldCall(call);
- }
- });
+ if (this is IHasCallHold holdCodec)
+ {
+ trilist.SetSigFalseAction(joinMap.HoldAllCalls.JoinNumber, () =>
+ {
+ foreach (var call in ActiveCalls)
+ {
+ holdCodec.HoldCall(call);
+ }
+ });
- for (int i = 0; i < joinMap.HoldCallsStart.JoinSpan; i++)
- {
- var index = i;
+ for (int i = 0; i < joinMap.HoldCallsStart.JoinSpan; i++)
+ {
+ var index = i;
- trilist.SetSigFalseAction((uint)(joinMap.HoldCallsStart.JoinNumber + index), () =>
- {
- if (index < 0 || index >= ActiveCalls.Count) return;
+ trilist.SetSigFalseAction((uint)(joinMap.HoldCallsStart.JoinNumber + index), () =>
+ {
+ if (index < 0 || index >= ActiveCalls.Count) return;
- var call = ActiveCalls[index];
- if (call != null)
- {
- holdCodec.HoldCall(call);
- }
- else
- {
- Debug.LogMessage(LogEventLevel.Information, this, "[Hold Call] Unable to find call at index '{0}'", i);
- }
- });
+ var call = ActiveCalls[index];
+ if (call != null)
+ {
+ holdCodec.HoldCall(call);
+ }
+ else
+ {
+ Debug.LogMessage(LogEventLevel.Information, this, "[Hold Call] Unable to find call at index '{0}'", i);
+ }
+ });
- trilist.SetSigFalseAction((uint)(joinMap.ResumeCallsStart.JoinNumber + index), () =>
- {
- if (index < 0 || index >= ActiveCalls.Count) return;
+ trilist.SetSigFalseAction((uint)(joinMap.ResumeCallsStart.JoinNumber + index), () =>
+ {
+ if (index < 0 || index >= ActiveCalls.Count) return;
- var call = ActiveCalls[index];
- if (call != null)
- {
- holdCodec.ResumeCall(call);
- }
- else
- {
- Debug.LogMessage(LogEventLevel.Information, this, "[Resume Call] Unable to find call at index '{0}'", i);
- }
- });
- }
- }
+ var call = ActiveCalls[index];
+ if (call != null)
+ {
+ holdCodec.ResumeCall(call);
+ }
+ else
+ {
+ Debug.LogMessage(LogEventLevel.Information, this, "[Resume Call] Unable to find call at index '{0}'", i);
+ }
+ });
+ }
+ }
- trilist.OnlineStatusChange += (device, args) =>
+ trilist.OnlineStatusChange += (device, args) =>
{
if (!args.DeviceOnLine) return;
@@ -1505,48 +1484,45 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
codec.CameraAutoModeIsOnFeedback.OutputChange += (o, a) =>
{
- var offCodec = codec as IHasCameraOff;
+ if (codec is IHasCameraOff offCodec)
+ {
+ if (offCodec.CameraIsOffFeedback.BoolValue)
+ {
+ trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, false);
+ trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false);
+ trilist.SetBool(joinMap.CameraModeOff.JoinNumber, true);
+ return;
+ }
- if (offCodec != null)
- {
- if (offCodec.CameraIsOffFeedback.BoolValue)
- {
- trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, false);
- trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false);
- trilist.SetBool(joinMap.CameraModeOff.JoinNumber, true);
- return;
- }
+ trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, a.BoolValue);
+ trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !a.BoolValue);
+ trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false);
+ return;
+ }
- trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, a.BoolValue);
- trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !a.BoolValue);
- trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false);
- return;
- }
-
- trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, a.BoolValue);
+ trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, a.BoolValue);
trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !a.BoolValue);
trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false);
};
- var offModeCodec = codec as IHasCameraOff;
- if (offModeCodec != null)
- {
- if (offModeCodec.CameraIsOffFeedback.BoolValue)
- {
- trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, false);
- trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false);
- trilist.SetBool(joinMap.CameraModeOff.JoinNumber, true);
- return;
- }
+ if (codec is IHasCameraOff offModeCodec)
+ {
+ if (offModeCodec.CameraIsOffFeedback.BoolValue)
+ {
+ trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, false);
+ trilist.SetBool(joinMap.CameraModeManual.JoinNumber, false);
+ trilist.SetBool(joinMap.CameraModeOff.JoinNumber, true);
+ return;
+ }
- trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, codec.CameraAutoModeIsOnFeedback.BoolValue);
- trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !codec.CameraAutoModeIsOnFeedback.BoolValue);
- trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false);
- return;
- }
+ trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, codec.CameraAutoModeIsOnFeedback.BoolValue);
+ trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !codec.CameraAutoModeIsOnFeedback.BoolValue);
+ trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false);
+ return;
+ }
- trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, codec.CameraAutoModeIsOnFeedback.BoolValue);
+ trilist.SetBool(joinMap.CameraModeAuto.JoinNumber, codec.CameraAutoModeIsOnFeedback.BoolValue);
trilist.SetBool(joinMap.CameraModeManual.JoinNumber, !codec.CameraAutoModeIsOnFeedback.BoolValue);
trilist.SetBool(joinMap.CameraModeOff.JoinNumber, false);
}
@@ -1565,64 +1541,58 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
trilist.SetBoolSigAction(joinMap.CameraTiltUp.JoinNumber, (b) =>
{
if (codec.SelectedCamera == null) return;
- var camera = codec.SelectedCamera as IHasCameraPtzControl;
- if (camera == null) return;
+ if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return;
- if (b) camera.TiltUp();
+ if (b) camera.TiltUp();
else camera.TiltStop();
});
trilist.SetBoolSigAction(joinMap.CameraTiltDown.JoinNumber, (b) =>
{
if (codec.SelectedCamera == null) return;
- var camera = codec.SelectedCamera as IHasCameraPtzControl;
- if (camera == null) return;
+ if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return;
- if (b) camera.TiltDown();
+ if (b) camera.TiltDown();
else camera.TiltStop();
});
trilist.SetBoolSigAction(joinMap.CameraPanLeft.JoinNumber, (b) =>
{
if (codec.SelectedCamera == null) return;
- var camera = codec.SelectedCamera as IHasCameraPtzControl;
- if (camera == null) return;
+ if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return;
- if (b) camera.PanLeft();
+ if (b) camera.PanLeft();
else camera.PanStop();
});
trilist.SetBoolSigAction(joinMap.CameraPanRight.JoinNumber, (b) =>
{
if (codec.SelectedCamera == null) return;
- var camera = codec.SelectedCamera as IHasCameraPtzControl;
- if (camera == null) return;
+ if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return;
- if (b) camera.PanRight();
+ if (b) camera.PanRight();
else camera.PanStop();
});
trilist.SetBoolSigAction(joinMap.CameraZoomIn.JoinNumber, (b) =>
{
if (codec.SelectedCamera == null) return;
- var camera = codec.SelectedCamera as IHasCameraPtzControl;
- if (camera == null) return;
+ if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return;
- if (b) camera.ZoomIn();
+ if (b) camera.ZoomIn();
else camera.ZoomStop();
});
trilist.SetBoolSigAction(joinMap.CameraZoomOut.JoinNumber, (b) =>
{
if (codec.SelectedCamera == null) return;
- var camera = codec.SelectedCamera as IHasCameraPtzControl;
- if (camera == null) return;
+ if (!(codec.SelectedCamera is IHasCameraPtzControl camera)) return;
- if (b) camera.ZoomOut();
+ if (b) camera.ZoomOut();
else camera.ZoomStop();
});
@@ -1630,9 +1600,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
trilist.SetBoolSigAction(joinMap.CameraFocusNear.JoinNumber, (b) =>
{
if (codec.SelectedCamera == null) return;
- var camera = codec.SelectedCamera as IHasCameraFocusControl;
- if (camera == null) return;
+ if (!(codec.SelectedCamera is IHasCameraFocusControl camera)) return;
if (b) camera.FocusNear();
else camera.FocusStop();
@@ -1641,9 +1610,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
trilist.SetBoolSigAction(joinMap.CameraFocusFar.JoinNumber, (b) =>
{
if (codec.SelectedCamera == null) return;
- var camera = codec.SelectedCamera as IHasCameraFocusControl;
- if (camera == null) return;
+ if (!(codec.SelectedCamera is IHasCameraFocusControl camera)) return;
if (b) camera.FocusFar();
else camera.FocusStop();
@@ -1652,9 +1620,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
trilist.SetSigFalseAction(joinMap.CameraFocusAuto.JoinNumber, () =>
{
if (codec.SelectedCamera == null) return;
- var camera = codec.SelectedCamera as IHasCameraFocusControl;
- if (camera == null) return;
+ if (!(codec.SelectedCamera is IHasCameraFocusControl camera)) return;
camera.TriggerAutoFocus();
});
@@ -1773,7 +1740,6 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
// Following fields only used for Bridging
private int _selectedRecentCallItemIndex;
- private CodecCallHistory.CallHistoryEntry _selectedRecentCallItem;
private DirectoryItem _selectedDirectoryItem;
private void LinkVideoCodecCallHistoryToApi(IHasCallHistory codec, BasicTriList trilist, VideoCodecControllerJoinMap joinMap)
@@ -1820,7 +1786,7 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
{
// Clear out selected item
_selectedRecentCallItemIndex = 0;
- _selectedRecentCallItem = null;
+
trilist.SetUshort(joinMap.SelectRecentCallItem.JoinNumber, 0);
trilist.SetString(joinMap.SelectedRecentCallName.JoinNumber, string.Empty);
trilist.SetString(joinMap.SelectedRecentCallNumber.JoinNumber, string.Empty);
@@ -1929,12 +1895,8 @@ namespace PepperDash.Essentials.Devices.Common.VideoCodec
{
if (value == true)
{
- var handler = InitialSyncCompleted;
- if (handler != null)
- {
- handler(this, new EventArgs());
- }
- }
+ InitialSyncCompleted?.Invoke(this, new EventArgs());
+ }
_InitialSyncComplete = value;
}
}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs b/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs
new file mode 100644
index 00000000..e555f11f
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs
@@ -0,0 +1,31 @@
+using Newtonsoft.Json;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.AppServer
+{
+ public class SourceSelectMessageContent
+ {
+
+ [JsonProperty("sourceListItemKey")]
+ public string SourceListItemKey { get; set; }
+ [JsonProperty("sourceListKey")]
+ public string SourceListKey { get; set; }
+ }
+
+ public class DirectRoute
+ {
+
+ [JsonProperty("sourceKey")]
+ public string SourceKey { get; set; }
+ [JsonProperty("destinationKey")]
+ public string DestinationKey { get; set; }
+ [JsonProperty("signalType")]
+ public eRoutingSignalType SignalType { get; set; }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ public delegate void PressAndHoldAction(bool b);
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs
new file mode 100644
index 00000000..cc09a637
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs
@@ -0,0 +1,60 @@
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Core.Logging;
+using PepperDash.Essentials.AppServer;
+using PepperDash.Essentials.AppServer.Messengers;
+using System.Linq;
+using DisplayBase = PepperDash.Essentials.Devices.Common.Displays.DisplayBase;
+
+namespace PepperDash.Essentials.Room.MobileControl
+{
+ public class DisplayBaseMessenger : MessengerBase
+ {
+ private readonly DisplayBase display;
+
+ public DisplayBaseMessenger(string key, string messagePath, DisplayBase device) : base(key, messagePath, device)
+ {
+ display = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ /*AddAction("/powerOn", (id, content) => display.PowerOn());
+ AddAction("/powerOff", (id, content) => display.PowerOff());
+ AddAction("/powerToggle", (id, content) => display.PowerToggle());*/
+
+ AddAction("/inputSelect", (id, content) =>
+ {
+ var s = content.ToObject>();
+
+ var inputPort = display.InputPorts.FirstOrDefault(i => i.Key == s.Value);
+
+ if (inputPort == null)
+ {
+ this.LogWarning("No input named {inputName} found for {deviceKey}", s, display.Key);
+ return;
+ }
+
+ display.ExecuteSwitch(inputPort.Selector);
+ });
+
+ AddAction("/inputs", (id, content) =>
+ {
+ var inputsList = display.InputPorts.Select(p => p.Key).ToList();
+
+ var messageObject = new MobileControlMessage
+ {
+ Type = MessagePath + "/inputs",
+ Content = JToken.FromObject(new
+ {
+ inputKeys = inputsList,
+ })
+ };
+
+ AppServerController.SendMessageObject(messageObject);
+ });
+ }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs
new file mode 100644
index 00000000..4ba89800
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs
@@ -0,0 +1,29 @@
+using PepperDash.Core;
+using PepperDash.Essentials.AppServer.Messengers;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.Room.MobileControl
+{
+ public class IChannelMessenger : MessengerBase
+ {
+ private readonly IChannel channelDevice;
+
+ public IChannelMessenger(string key, string messagePath, IChannel device) : base(key, messagePath, device as IKeyName)
+ {
+ channelDevice = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/chanUp", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => channelDevice?.ChannelUp(b)));
+
+ AddAction("/chanDown", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => channelDevice?.ChannelDown(b)));
+ AddAction("/lastChan", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => channelDevice?.LastChannel(b)));
+ AddAction("/guide", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => channelDevice?.Guide(b)));
+ AddAction("/info", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => channelDevice?.Info(b)));
+ AddAction("/exit", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => channelDevice?.Exit(b)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs
new file mode 100644
index 00000000..86df2590
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs
@@ -0,0 +1,25 @@
+using PepperDash.Core;
+using PepperDash.Essentials.AppServer.Messengers;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.Room.MobileControl
+{
+ public class IColorMessenger : MessengerBase
+ {
+ private readonly IColor colorDevice;
+ public IColorMessenger(string key, string messagePath, IColor device) : base(key, messagePath, device as IKeyName)
+ {
+ colorDevice = device as IColor;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/red", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => colorDevice?.Red(b)));
+ AddAction("/green", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => colorDevice?.Green(b)));
+ AddAction("/yellow", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => colorDevice?.Yellow(b)));
+ AddAction("/blue", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => colorDevice?.Blue(b)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs
new file mode 100644
index 00000000..4af07703
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs
@@ -0,0 +1,29 @@
+using PepperDash.Core;
+using PepperDash.Essentials.AppServer.Messengers;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.Room.MobileControl
+{
+ public class IDPadMessenger : MessengerBase
+ {
+ private readonly IDPad dpadDevice;
+ public IDPadMessenger(string key, string messagePath, IDPad device) : base(key, messagePath, device as IKeyName)
+ {
+ dpadDevice = device;
+ }
+
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/up", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => dpadDevice?.Up(b)));
+ AddAction("/down", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => dpadDevice?.Down(b)));
+ AddAction("/left", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => dpadDevice?.Left(b)));
+ AddAction("/right", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => dpadDevice?.Right(b)));
+ AddAction("/select", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => dpadDevice?.Select(b)));
+ AddAction("/menu", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => dpadDevice?.Menu(b)));
+ AddAction("/exit", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => dpadDevice?.Exit(b)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs
new file mode 100644
index 00000000..8e286979
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs
@@ -0,0 +1,24 @@
+using PepperDash.Core;
+using PepperDash.Essentials.AppServer.Messengers;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.Room.MobileControl
+{
+ public class IDvrMessenger : MessengerBase
+ {
+ private readonly IDvr dvrDevice;
+ public IDvrMessenger(string key, string messagePath, IDvr device) : base(key, messagePath, device as IKeyName)
+ {
+ dvrDevice = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/dvrlist", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => dvrDevice?.DvrList(b)));
+ AddAction("/record", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => dvrDevice?.Record(b)));
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs
new file mode 100644
index 00000000..39ed0e6f
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs
@@ -0,0 +1,24 @@
+using PepperDash.Core;
+using PepperDash.Essentials.AppServer.Messengers;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.Room.MobileControl
+{
+ public class IHasPowerMessenger : MessengerBase
+ {
+ private readonly IHasPowerControl powerDevice;
+ public IHasPowerMessenger(string key, string messagePath, IHasPowerControl device) : base(key, messagePath, device as IKeyName)
+ {
+ powerDevice = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/powerOn", (id, content) => powerDevice?.PowerOn());
+ AddAction("/powerOff", (id, content) => powerDevice?.PowerOff());
+ AddAction("/powerToggle", (id, content) => powerDevice?.PowerToggle());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs
new file mode 100644
index 00000000..69b5bc9d
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs
@@ -0,0 +1,34 @@
+using PepperDash.Core;
+using PepperDash.Essentials.AppServer.Messengers;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.Room.MobileControl
+{
+ public class INumericKeypadMessenger : MessengerBase
+ {
+ private readonly INumericKeypad keypadDevice;
+ public INumericKeypadMessenger(string key, string messagePath, INumericKeypad device) : base(key, messagePath, device as IKeyName)
+ {
+ keypadDevice = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/num0", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit0(b)));
+ AddAction("/num1", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit1(b)));
+ AddAction("/num2", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit2(b)));
+ AddAction("/num3", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit3(b)));
+ AddAction("/num4", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit4(b)));
+ AddAction("/num5", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit5(b)));
+ AddAction("/num6", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit6(b)));
+ AddAction("/num7", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit7(b)));
+ AddAction("/num8", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit8(b)));
+ AddAction("/num9", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.Digit9(b)));
+ AddAction("/numDash", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.KeypadAccessoryButton1(b)));
+ AddAction("/numEnter", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => keypadDevice?.KeypadAccessoryButton2(b)));
+ // Deal with the Accessory functions on the numpad later
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs
new file mode 100644
index 00000000..0e7c227b
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs
@@ -0,0 +1,39 @@
+using PepperDash.Core;
+using PepperDash.Essentials.AppServer.Messengers;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.Room.MobileControl
+{
+ public class ISetTopBoxControlsMessenger : MessengerBase
+ {
+ private readonly ISetTopBoxControls stbDevice;
+ public ISetTopBoxControlsMessenger(string key, string messagePath, ISetTopBoxControls device) : base(key, messagePath, device as IKeyName)
+ {
+ stbDevice = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+ AddAction("/fullStatus", (id, content) => SendISetTopBoxControlsFullMessageObject());
+ AddAction("/dvrList", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => stbDevice?.DvrList(b)));
+ AddAction("/replay", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => stbDevice?.Replay(b)));
+ }
+ ///
+ /// Helper method to build call status for vtc
+ ///
+ ///
+ private void SendISetTopBoxControlsFullMessageObject()
+ {
+
+ PostStatusMessage(new SetTopBoxControlsState());
+
+
+ }
+ }
+
+ public class SetTopBoxControlsState : DeviceStateMessageBase
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs
new file mode 100644
index 00000000..75f74418
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs
@@ -0,0 +1,30 @@
+using PepperDash.Core;
+using PepperDash.Essentials.AppServer.Messengers;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.Room.MobileControl
+{
+ public class ITransportMessenger : MessengerBase
+ {
+ private readonly ITransport transportDevice;
+ public ITransportMessenger(string key, string messagePath, ITransport device) : base(key, messagePath, device as IKeyName)
+ {
+ transportDevice = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/play", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => transportDevice?.Play(b)));
+ AddAction("/pause", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => transportDevice?.Pause(b)));
+ AddAction("/stop", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => transportDevice?.Stop(b)));
+ AddAction("/prevTrack", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => transportDevice?.ChapPlus(b)));
+ AddAction("/nextTrack", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => transportDevice?.ChapMinus(b)));
+ AddAction("/rewind", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => transportDevice?.Rewind(b)));
+ AddAction("/ffwd", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => transportDevice?.FFwd(b)));
+ AddAction("/record", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => transportDevice?.Record(b)));
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs
new file mode 100644
index 00000000..9a42141e
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs
@@ -0,0 +1,116 @@
+using Newtonsoft.Json.Linq;
+using PepperDash.Essentials.Devices.Common.AudioCodec;
+using PepperDash.Essentials.Devices.Common.Codec;
+using System;
+using System.Linq;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ ///
+ /// Provides a messaging bridge for an AudioCodecBase device
+ ///
+ public class AudioCodecBaseMessenger : MessengerBase
+ {
+ ///
+ /// Device being bridged
+ ///
+ public AudioCodecBase Codec { get; private set; }
+
+ ///
+ /// Constuctor
+ ///
+ ///
+ ///
+ ///
+ public AudioCodecBaseMessenger(string key, AudioCodecBase codec, string messagePath)
+ : base(key, messagePath, codec)
+ {
+ Codec = codec ?? throw new ArgumentNullException("codec");
+ codec.CallStatusChange += Codec_CallStatusChange;
+ }
+
+ protected override void RegisterActions()
+
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) => SendAtcFullMessageObject());
+ AddAction("/dial", (id, content) =>
+ {
+ var msg = content.ToObject>();
+
+ Codec.Dial(msg.Value);
+ });
+
+ AddAction("/endCallById", (id, content) =>
+ {
+ var msg = content.ToObject>();
+
+ var call = GetCallWithId(msg.Value);
+ if (call != null)
+ Codec.EndCall(call);
+ });
+
+ AddAction("/endAllCalls", (id, content) => Codec.EndAllCalls());
+ AddAction("/dtmf", (id, content) =>
+ {
+ var msg = content.ToObject>();
+
+ Codec.SendDtmf(msg.Value);
+ });
+
+ AddAction("/rejectById", (id, content) =>
+ {
+ var msg = content.ToObject>();
+
+ var call = GetCallWithId(msg.Value);
+
+ if (call != null)
+ Codec.RejectCall(call);
+ });
+
+ AddAction("/acceptById", (id, content) =>
+ {
+ var msg = content.ToObject>();
+ var call = GetCallWithId(msg.Value);
+ if (call != null)
+ Codec.AcceptCall(call);
+ });
+ }
+
+ ///
+ /// Helper to grab a call with string ID
+ ///
+ ///
+ ///
+ private CodecActiveCallItem GetCallWithId(string id)
+ {
+ return Codec.ActiveCalls.FirstOrDefault(c => c.Id == id);
+ }
+
+ private void Codec_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e)
+ {
+ SendAtcFullMessageObject();
+ }
+
+ ///
+ /// Helper method to build call status for vtc
+ ///
+ ///
+ private void SendAtcFullMessageObject()
+ {
+ var info = Codec.CodecInfo;
+
+ PostStatusMessage(JToken.FromObject(new
+ {
+ isInCall = Codec.IsInCall,
+ calls = Codec.ActiveCalls,
+ info = new
+ {
+ phoneNumber = info.PhoneNumber
+ }
+ })
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs
new file mode 100644
index 00000000..36a94781
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs
@@ -0,0 +1,204 @@
+using Newtonsoft.Json.Linq;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Devices.Common.Cameras;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class CameraBaseMessenger : MessengerBase
+ {
+ ///
+ /// Device being bridged
+ ///
+ public CameraBase Camera { get; set; }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ ///
+ public CameraBaseMessenger(string key, CameraBase camera, string messagePath)
+ : base(key, messagePath, camera)
+ {
+ Camera = camera ?? throw new ArgumentNullException("camera");
+
+
+ if (Camera is IHasCameraPresets presetsCamera)
+ {
+ presetsCamera.PresetsListHasChanged += PresetsCamera_PresetsListHasChanged;
+ }
+ }
+
+ private void PresetsCamera_PresetsListHasChanged(object sender, EventArgs e)
+ {
+ var presetList = new List();
+
+ if (Camera is IHasCameraPresets presetsCamera)
+ presetList = presetsCamera.Presets;
+
+ PostStatusMessage(JToken.FromObject(new
+ {
+ presets = presetList
+ })
+ );
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) => SendCameraFullMessageObject());
+
+
+ if (Camera is IHasCameraPtzControl ptzCamera)
+ {
+ // Need to evaluate how to pass through these P&H actions. Need a method that takes a bool maybe?
+ AddAction("/cameraUp", (id, content) => HandleCameraPressAndHold(content, (b) =>
+ {
+ if (b)
+ {
+ ptzCamera.TiltUp();
+ return;
+ }
+
+ ptzCamera.TiltStop();
+ }));
+ AddAction("/cameraDown", (id, content) => HandleCameraPressAndHold(content, (b) =>
+ {
+ if (b)
+ {
+ ptzCamera.TiltDown();
+ return;
+ }
+
+ ptzCamera.TiltStop();
+ }));
+ AddAction("/cameraLeft", (id, content) => HandleCameraPressAndHold(content, (b) =>
+ {
+ if (b)
+ {
+ ptzCamera.PanLeft();
+ return;
+ }
+
+ ptzCamera.PanStop();
+ }));
+ AddAction("/cameraRight", (id, content) => HandleCameraPressAndHold(content, (b) =>
+ {
+ if (b)
+ {
+ ptzCamera.PanRight();
+ return;
+ }
+
+ ptzCamera.PanStop();
+ }));
+ AddAction("/cameraZoomIn", (id, content) => HandleCameraPressAndHold(content, (b) =>
+ {
+ if (b)
+ {
+ ptzCamera.ZoomIn();
+ return;
+ }
+
+ ptzCamera.ZoomStop();
+ }));
+ AddAction("/cameraZoomOut", (id, content) => HandleCameraPressAndHold(content, (b) =>
+ {
+ if (b)
+ {
+ ptzCamera.ZoomOut();
+ return;
+ }
+
+ ptzCamera.ZoomStop();
+ }));
+ }
+
+ if (Camera is IHasCameraAutoMode)
+ {
+ AddAction("/cameraModeAuto", (id, content) => (Camera as IHasCameraAutoMode).CameraAutoModeOn());
+
+ AddAction("/cameraModeManual", (id, content) => (Camera as IHasCameraAutoMode).CameraAutoModeOff());
+
+ }
+
+ if (Camera is IHasPowerControl)
+ {
+ AddAction("/cameraModeOff", (id, content) => (Camera as IHasPowerControl).PowerOff());
+ AddAction("/cameraModeManual", (id, content) => (Camera as IHasPowerControl).PowerOn());
+ }
+
+
+ if (Camera is IHasCameraPresets presetsCamera)
+ {
+ for (int i = 1; i <= 6; i++)
+ {
+ var preset = i;
+ AddAction("/cameraPreset" + i, (id, content) =>
+ {
+ var msg = content.ToObject>();
+
+ presetsCamera.PresetSelect(msg.Value);
+ });
+
+ }
+ }
+ }
+
+ private void HandleCameraPressAndHold(JToken content, Action cameraAction)
+ {
+ var state = content.ToObject>();
+
+ var timerHandler = PressAndHoldHandler.GetPressAndHoldHandler(state.Value);
+ if (timerHandler == null)
+ {
+ return;
+ }
+
+ timerHandler(state.Value, cameraAction);
+
+ cameraAction(state.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ ///
+ /// Helper method to update the full status of the camera
+ ///
+ private void SendCameraFullMessageObject()
+ {
+ var presetList = new List();
+
+ if (Camera is IHasCameraPresets presetsCamera)
+ presetList = presetsCamera.Presets;
+
+ PostStatusMessage(JToken.FromObject(new
+ {
+ cameraManualSupported = Camera is IHasCameraControls,
+ cameraAutoSupported = Camera is IHasCameraAutoMode,
+ cameraOffSupported = Camera is IHasCameraOff,
+ cameraMode = GetCameraMode(),
+ hasPresets = Camera is IHasCameraPresets,
+ presets = presetList
+ })
+ );
+ }
+
+ ///
+ /// Computes the current camera mode
+ ///
+ ///
+ private string GetCameraMode()
+ {
+ string m;
+ if (Camera is IHasCameraAutoMode && (Camera as IHasCameraAutoMode).CameraAutoModeIsOnFeedback.BoolValue)
+ m = eCameraControlMode.Auto.ToString().ToLower();
+ else if (Camera is IHasPowerControlWithFeedback && !(Camera as IHasPowerControlWithFeedback).PowerIsOnFeedback.BoolValue)
+ m = eCameraControlMode.Off.ToString().ToLower();
+ else
+ m = eCameraControlMode.Manual.ToString().ToLower();
+ return m;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs
new file mode 100644
index 00000000..ffd58948
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs
@@ -0,0 +1,42 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core.DeviceInfo;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class DeviceInfoMessenger : MessengerBase
+ {
+ private readonly IDeviceInfoProvider _deviceInfoProvider;
+ public DeviceInfoMessenger(string key, string messagePath, IDeviceInfoProvider device) : base(key, messagePath, device as Device)
+ {
+ _deviceInfoProvider = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ _deviceInfoProvider.DeviceInfoChanged += (o, a) =>
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ deviceInfo = a.DeviceInfo
+ }));
+ };
+
+ AddAction("/fullStatus", (id, context) => PostStatusMessage(new DeviceInfoStateMessage
+ {
+ DeviceInfo = _deviceInfoProvider.DeviceInfo
+ }));
+
+ AddAction("/update", (id, context) => _deviceInfoProvider.UpdateDeviceInfo());
+ }
+ }
+
+ public class DeviceInfoStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("deviceInfo")]
+ public DeviceInfo DeviceInfo { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs
new file mode 100644
index 00000000..148b7d5b
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs
@@ -0,0 +1,100 @@
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Core.Logging;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+using PepperDash.Essentials.Core.Presets;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class DevicePresetsModelMessenger : MessengerBase
+ {
+ private readonly ITvPresetsProvider _presetsDevice;
+
+ public DevicePresetsModelMessenger(string key, string messagePath, ITvPresetsProvider presetsDevice)
+ : base(key, messagePath, presetsDevice as Device)
+ {
+ _presetsDevice = presetsDevice;
+ }
+
+ private void SendPresets()
+ {
+ PostStatusMessage(new PresetStateMessage
+ {
+ Favorites = _presetsDevice.TvPresets.PresetsList
+ });
+ }
+
+ private void RecallPreset(ISetTopBoxNumericKeypad device, string channel)
+ {
+ _presetsDevice.TvPresets.Dial(channel, device);
+ }
+
+ private void SavePresets(List presets)
+ {
+ _presetsDevice.TvPresets.UpdatePresets(presets);
+ }
+
+
+ #region Overrides of MessengerBase
+
+ protected override void RegisterActions()
+
+ {
+ AddAction("/presets/fullStatus", (id, content) =>
+ {
+ this.LogInformation("getting full status for client {id}", id);
+ try
+ {
+ SendPresets();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Exception sending preset full status", this);
+ }
+ });
+
+ AddAction("/presets/recall", (id, content) =>
+ {
+ var p = content.ToObject();
+
+
+ if (!(DeviceManager.GetDeviceForKey(p.DeviceKey) is ISetTopBoxNumericKeypad dev))
+ {
+ this.LogDebug("Unable to find device with key {0}", p.DeviceKey);
+ return;
+ }
+
+ RecallPreset(dev, p.Preset.Channel);
+ });
+
+ AddAction("/presets/save", (id, content) =>
+ {
+ var presets = content.ToObject>();
+
+ SavePresets(presets);
+ });
+
+ _presetsDevice.TvPresets.PresetsSaved += (p) => SendPresets();
+ }
+
+ #endregion
+ }
+
+ public class PresetChannelMessage
+ {
+ [JsonProperty("preset")]
+ public PresetChannel Preset;
+
+ [JsonProperty("deviceKey")]
+ public string DeviceKey;
+ }
+
+ public class PresetStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("favorites", NullValueHandling = NullValueHandling.Ignore)]
+ public List Favorites { get; set; } = new List();
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs
new file mode 100644
index 00000000..22f837c3
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs
@@ -0,0 +1,172 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+using System;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class DeviceVolumeMessenger : MessengerBase
+ {
+ private readonly IBasicVolumeWithFeedback _localDevice;
+
+ public DeviceVolumeMessenger(string key, string messagePath, IBasicVolumeWithFeedback device)
+ : base(key, messagePath, device as IKeyName)
+ {
+ _localDevice = device;
+ }
+
+ private void SendStatus()
+ {
+ try
+ {
+ var messageObj = new VolumeStateMessage
+ {
+ Volume = new Volume
+ {
+ Level = _localDevice?.VolumeLevelFeedback.IntValue ?? -1,
+ Muted = _localDevice?.MuteFeedback.BoolValue ?? false,
+ HasMute = true, // assume all devices have mute for now
+ }
+ };
+
+ if (_localDevice is IBasicVolumeWithFeedbackAdvanced volumeAdvanced)
+ {
+ messageObj.Volume.RawValue = volumeAdvanced.RawVolumeLevel.ToString();
+ messageObj.Volume.Units = volumeAdvanced.Units;
+ }
+
+ PostStatusMessage(messageObj);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Exception sending full status", this);
+ }
+ }
+
+ #region Overrides of MessengerBase
+
+
+ protected override void RegisterActions()
+ {
+ AddAction("/fullStatus", (id, content) => SendStatus());
+
+ AddAction("/level", (id, content) =>
+ {
+ var volume = content.ToObject>();
+
+ _localDevice.SetVolume(volume.Value);
+ });
+
+ AddAction("/muteToggle", (id, content) =>
+ {
+ _localDevice.MuteToggle();
+ });
+
+ AddAction("/muteOn", (id, content) =>
+ {
+ _localDevice.MuteOn();
+ });
+
+ AddAction("/muteOff", (id, content) =>
+ {
+ _localDevice.MuteOff();
+ });
+
+ AddAction("/volumeUp", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) =>
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Calling {localDevice} volume up with {value}", DeviceKey, b);
+ try
+ {
+ _localDevice.VolumeUp(b);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Got exception during volume up: {Exception}", null, ex);
+ }
+ }));
+
+
+
+ AddAction("/volumeDown", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) =>
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Calling {localDevice} volume down with {value}", DeviceKey, b);
+
+ try
+ {
+ _localDevice.VolumeDown(b);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Got exception during volume down: {Exception}", null, ex);
+ }
+ }));
+
+ _localDevice.MuteFeedback.OutputChange += (sender, args) =>
+ {
+ PostStatusMessage(JToken.FromObject(
+ new
+ {
+ volume = new
+ {
+ muted = args.BoolValue
+ }
+ })
+ );
+ };
+
+ _localDevice.VolumeLevelFeedback.OutputChange += (sender, args) =>
+ {
+ var rawValue = "";
+ if (_localDevice is IBasicVolumeWithFeedbackAdvanced volumeAdvanced)
+ {
+ rawValue = volumeAdvanced.RawVolumeLevel.ToString();
+ }
+
+ var message = new
+ {
+ volume = new
+ {
+ level = args.IntValue,
+ rawValue
+ }
+ };
+
+ PostStatusMessage(JToken.FromObject(message));
+ };
+
+
+ }
+
+ #endregion
+ }
+
+ public class VolumeStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("volume", NullValueHandling = NullValueHandling.Ignore)]
+ public Volume Volume { get; set; }
+ }
+
+ public class Volume
+ {
+ [JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)]
+ public int? Level { get; set; }
+
+ [JsonProperty("hasMute", NullValueHandling = NullValueHandling.Ignore)]
+ public bool? HasMute { get; set; }
+
+ [JsonProperty("muted", NullValueHandling = NullValueHandling.Ignore)]
+ public bool? Muted { get; set; }
+
+ [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)]
+ public string Label { get; set; }
+
+ [JsonProperty("rawValue", NullValueHandling = NullValueHandling.Ignore)]
+ public string RawValue { get; set; }
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ [JsonProperty("units", NullValueHandling = NullValueHandling.Ignore)]
+ public eVolumeLevelUnits? Units { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs
new file mode 100644
index 00000000..64624bfa
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs
@@ -0,0 +1,25 @@
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class GenericMessenger : MessengerBase
+ {
+ public GenericMessenger(string key, EssentialsDevice device, string messagePath) : base(key, messagePath, device)
+ {
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) => SendFullStatus());
+ }
+
+ private void SendFullStatus()
+ {
+ var state = new DeviceStateMessageBase();
+
+ PostStatusMessage(state);
+ }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs
new file mode 100644
index 00000000..5ab81832
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs
@@ -0,0 +1,74 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class ICommunicationMonitorMessenger : MessengerBase
+ {
+ private readonly ICommunicationMonitor _communicationMonitor;
+
+ public ICommunicationMonitorMessenger(string key, string messagePath, ICommunicationMonitor device) : base(key, messagePath, device as IKeyName)
+ {
+ _communicationMonitor = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) =>
+ {
+ PostStatusMessage(new CommunicationMonitorState
+ {
+ CommunicationMonitor = new CommunicationMonitorProps
+ {
+ IsOnline = _communicationMonitor.CommunicationMonitor.IsOnline,
+ Status = _communicationMonitor.CommunicationMonitor.Status
+ }
+ });
+ });
+
+ _communicationMonitor.CommunicationMonitor.StatusChange += (sender, args) =>
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ commMonitor = new CommunicationMonitorProps
+ {
+ IsOnline = _communicationMonitor.CommunicationMonitor.IsOnline,
+ Status = _communicationMonitor.CommunicationMonitor.Status
+ }
+ }));
+ };
+ }
+ }
+
+ ///
+ /// Represents the state of the communication monitor
+ ///
+ public class CommunicationMonitorState : DeviceStateMessageBase
+ {
+ [JsonProperty("commMonitor", NullValueHandling = NullValueHandling.Ignore)]
+ public CommunicationMonitorProps CommunicationMonitor { get; set; }
+
+ }
+
+ public class CommunicationMonitorProps
+ { ///
+ /// For devices that implement ICommunicationMonitor, reports the online status of the device
+ ///
+ [JsonProperty("isOnline", NullValueHandling = NullValueHandling.Ignore)]
+ public bool? IsOnline { get; set; }
+
+ ///
+ /// For devices that implement ICommunicationMonitor, reports the online status of the device
+ ///
+ [JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)]
+ [JsonConverter(typeof(StringEnumConverter))]
+ public MonitorStatus Status { get; set; }
+
+ }
+
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs
new file mode 100644
index 00000000..e40cd8eb
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs
@@ -0,0 +1,50 @@
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class IDspPresetsMessenger : MessengerBase
+ {
+ private readonly IDspPresets device;
+
+ public IDspPresetsMessenger(string key, string messagePath, IDspPresets device)
+ : base(key, messagePath, device as IKeyName)
+ {
+ this.device = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) =>
+ {
+ var message = new IHasDspPresetsStateMessage
+ {
+ Presets = device.Presets
+ };
+
+ PostStatusMessage(message);
+ });
+
+ AddAction("/recallPreset", (id, content) =>
+ {
+ var presetKey = content.ToObject();
+
+
+ if (!string.IsNullOrEmpty(presetKey))
+ {
+ device.RecallPreset(presetKey);
+ }
+ });
+ }
+ }
+
+ public class IHasDspPresetsStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("presets")]
+ public Dictionary Presets { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs
new file mode 100644
index 00000000..a29d7b9e
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs
@@ -0,0 +1,154 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Core.Logging;
+using PepperDash.Essentials.Core;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class IEssentialsRoomCombinerMessenger : MessengerBase
+ {
+ private readonly IEssentialsRoomCombiner _roomCombiner;
+
+ public IEssentialsRoomCombinerMessenger(string key, string messagePath, IEssentialsRoomCombiner roomCombiner)
+ : base(key, messagePath, roomCombiner as IKeyName)
+ {
+ _roomCombiner = roomCombiner;
+ }
+
+ protected override void RegisterActions()
+ {
+ AddAction("/fullStatus", (id, content) => SendFullStatus());
+
+ AddAction("/setAutoMode", (id, content) =>
+ {
+ _roomCombiner.SetAutoMode();
+ });
+
+ AddAction("/setManualMode", (id, content) =>
+ {
+ _roomCombiner.SetManualMode();
+ });
+
+ AddAction("/toggleMode", (id, content) =>
+ {
+ _roomCombiner.ToggleMode();
+ });
+
+ AddAction("/togglePartitionState", (id, content) =>
+ {
+ try
+ {
+ var partitionKey = content.ToObject();
+
+ _roomCombiner.TogglePartitionState(partitionKey);
+ }
+ catch (Exception e)
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Error toggling partition state: {e}", this);
+ }
+ });
+
+ AddAction("/setRoomCombinationScenario", (id, content) =>
+ {
+ try
+ {
+ var scenarioKey = content.ToObject();
+
+ _roomCombiner.SetRoomCombinationScenario(scenarioKey);
+ }
+ catch (Exception e)
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Error toggling partition state: {e}", this);
+ }
+ });
+
+ _roomCombiner.RoomCombinationScenarioChanged += (sender, args) =>
+ {
+ SendFullStatus();
+ };
+
+ _roomCombiner.IsInAutoModeFeedback.OutputChange += (sender, args) =>
+ {
+ var message = new
+ {
+ isInAutoMode = _roomCombiner.IsInAutoModeFeedback.BoolValue
+ };
+
+ PostStatusMessage(JToken.FromObject(message));
+ };
+
+ foreach (var partition in _roomCombiner.Partitions)
+ {
+ partition.PartitionPresentFeedback.OutputChange += (sender, args) =>
+ {
+ var message = new
+ {
+ partitions = _roomCombiner.Partitions
+ };
+
+ PostStatusMessage(JToken.FromObject(message));
+ };
+ }
+ }
+
+ private void SendFullStatus()
+ {
+ try
+ {
+ var rooms = new List();
+
+ foreach (var room in _roomCombiner.Rooms)
+ {
+ rooms.Add(new RoomCombinerRoom { Key = room.Key, Name = room.Name });
+ }
+
+ var message = new IEssentialsRoomCombinerStateMessage
+ {
+ IsInAutoMode = _roomCombiner.IsInAutoMode,
+ CurrentScenario = _roomCombiner.CurrentScenario,
+ Rooms = rooms,
+ RoomCombinationScenarios = _roomCombiner.RoomCombinationScenarios,
+ Partitions = _roomCombiner.Partitions
+ };
+
+ PostStatusMessage(message);
+ }
+ catch (Exception e)
+ {
+ this.LogException(e, "Error sending full status");
+ }
+ }
+
+ private class RoomCombinerRoom : IKeyName
+ {
+ [JsonProperty("key")]
+ public string Key { get; set; }
+
+ [JsonProperty("name")]
+ public string Name { get; set; }
+ }
+ }
+
+ public class IEssentialsRoomCombinerStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("isInAutoMode", NullValueHandling = NullValueHandling.Ignore)]
+ public bool IsInAutoMode { get; set; }
+
+ [JsonProperty("currentScenario", NullValueHandling = NullValueHandling.Ignore)]
+ public IRoomCombinationScenario CurrentScenario { get; set; }
+
+ [JsonProperty("rooms", NullValueHandling = NullValueHandling.Ignore)]
+ public List Rooms { get; set; }
+
+ [JsonProperty("roomCombinationScenarios", NullValueHandling = NullValueHandling.Ignore)]
+ public List RoomCombinationScenarios { get; set; }
+
+ [JsonProperty("partitions", NullValueHandling = NullValueHandling.Ignore)]
+ public List Partitions { get; set; }
+ }
+
+
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs
new file mode 100644
index 00000000..24f1f461
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs
@@ -0,0 +1,57 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class IHasCurrentSourceInfoMessenger : MessengerBase
+ {
+ private readonly IHasCurrentSourceInfoChange sourceDevice;
+ public IHasCurrentSourceInfoMessenger(string key, string messagePath, IHasCurrentSourceInfoChange device) : base(key, messagePath, device as IKeyName)
+ {
+ sourceDevice = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) =>
+ {
+ var message = new CurrentSourceStateMessage
+ {
+ CurrentSourceKey = sourceDevice.CurrentSourceInfoKey,
+ CurrentSource = sourceDevice.CurrentSourceInfo
+ };
+
+ PostStatusMessage(message);
+ });
+
+ sourceDevice.CurrentSourceChange += (sender, e) =>
+ {
+ switch (e)
+ {
+ case ChangeType.DidChange:
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ currentSourceKey = string.IsNullOrEmpty(sourceDevice.CurrentSourceInfoKey) ? string.Empty : sourceDevice.CurrentSourceInfoKey,
+ currentSource = sourceDevice.CurrentSourceInfo
+ }));
+ break;
+ }
+ }
+ };
+ }
+ }
+
+ public class CurrentSourceStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("currentSourceKey", NullValueHandling = NullValueHandling.Ignore)]
+ public string CurrentSourceKey { get; set; }
+
+ [JsonProperty("currentSource")]
+ public SourceListItem CurrentSource { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs
new file mode 100644
index 00000000..7fb39c8c
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs
@@ -0,0 +1,52 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class IHasPowerControlWithFeedbackMessenger : MessengerBase
+ {
+ private readonly IHasPowerControlWithFeedback _powerControl;
+
+ public IHasPowerControlWithFeedbackMessenger(string key, string messagePath, IHasPowerControlWithFeedback powerControl)
+ : base(key, messagePath, powerControl as IKeyName)
+ {
+ _powerControl = powerControl;
+ }
+
+ public void SendFullStatus()
+ {
+ var messageObj = new PowerControlWithFeedbackStateMessage
+ {
+ PowerState = _powerControl.PowerIsOnFeedback.BoolValue
+ };
+
+ PostStatusMessage(messageObj);
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) => SendFullStatus());
+
+ _powerControl.PowerIsOnFeedback.OutputChange += PowerIsOnFeedback_OutputChange; ;
+ }
+
+ private void PowerIsOnFeedback_OutputChange(object sender, FeedbackEventArgs args)
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ powerState = args.BoolValue
+ })
+ );
+ }
+ }
+
+ public class PowerControlWithFeedbackStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("powerState", NullValueHandling = NullValueHandling.Ignore)]
+ public bool? PowerState { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs
new file mode 100644
index 00000000..80481470
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs
@@ -0,0 +1,81 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Devices.Common.Codec;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class IHasScheduleAwarenessMessenger : MessengerBase
+ {
+ public IHasScheduleAwareness ScheduleSource { get; private set; }
+
+ public IHasScheduleAwarenessMessenger(string key, IHasScheduleAwareness scheduleSource, string messagePath)
+ : base(key, messagePath, scheduleSource as IKeyName)
+ {
+ ScheduleSource = scheduleSource ?? throw new ArgumentNullException("scheduleSource");
+ ScheduleSource.CodecSchedule.MeetingsListHasChanged += new EventHandler(CodecSchedule_MeetingsListHasChanged);
+ ScheduleSource.CodecSchedule.MeetingEventChange += new EventHandler(CodecSchedule_MeetingEventChange);
+ }
+
+ protected override void RegisterActions()
+ {
+ AddAction("/schedule/fullStatus", (id, content) => SendFullScheduleObject());
+ }
+
+ private void CodecSchedule_MeetingEventChange(object sender, MeetingEventArgs e)
+ {
+ PostStatusMessage(JToken.FromObject(new MeetingChangeMessage
+ {
+ MeetingChange = new MeetingChange
+ {
+ ChangeType = e.ChangeType.ToString(),
+ Meeting = e.Meeting
+ }
+ })
+ );
+ }
+
+ private void CodecSchedule_MeetingsListHasChanged(object sender, EventArgs e)
+ {
+ SendFullScheduleObject();
+ }
+
+ ///
+ /// Helper method to send the full schedule data
+ ///
+ private void SendFullScheduleObject()
+ {
+ PostStatusMessage(new FullScheduleMessage
+ {
+ Meetings = ScheduleSource.CodecSchedule.Meetings,
+ MeetingWarningMinutes = ScheduleSource.CodecSchedule.MeetingWarningMinutes
+ });
+ }
+ }
+
+ public class FullScheduleMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("meetings", NullValueHandling = NullValueHandling.Ignore)]
+ public List Meetings { get; set; }
+
+ [JsonProperty("meetingWarningMinutes", NullValueHandling = NullValueHandling.Ignore)]
+ public int MeetingWarningMinutes { get; set; }
+ }
+
+ public class MeetingChangeMessage
+ {
+ [JsonProperty("meetingChange", NullValueHandling = NullValueHandling.Ignore)]
+ public MeetingChange MeetingChange { get; set; }
+ }
+
+ public class MeetingChange
+ {
+ [JsonProperty("changeType", NullValueHandling = NullValueHandling.Ignore)]
+ public string ChangeType { get; set; }
+
+ [JsonProperty("meeting", NullValueHandling = NullValueHandling.Ignore)]
+ public Meeting Meeting { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs
new file mode 100644
index 00000000..c44ec9ae
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs
@@ -0,0 +1,43 @@
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+using System;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class IHumiditySensorMessenger : MessengerBase
+ {
+ private readonly IHumiditySensor device;
+
+ public IHumiditySensorMessenger(string key, IHumiditySensor device, string messagePath)
+ : base(key, messagePath, device as IKeyName)
+ {
+ this.device = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) => SendFullStatus());
+
+ device.HumidityFeedback.OutputChange += new EventHandler((o, a) => SendFullStatus());
+ }
+
+ private void SendFullStatus()
+ {
+ var state = new IHumiditySensorStateMessage
+ {
+ Humidity = string.Format("{0}%", device.HumidityFeedback.UShortValue)
+ };
+
+ PostStatusMessage(state);
+ }
+ }
+
+ public class IHumiditySensorStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("humidity")]
+ public string Humidity { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs
new file mode 100644
index 00000000..4fd3515a
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs
@@ -0,0 +1,91 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class ILevelControlsMessenger : MessengerBase
+ {
+ private ILevelControls levelControlsDevice;
+ public ILevelControlsMessenger(string key, string messagePath, ILevelControls device) : base(key, messagePath, device as IKeyName)
+ {
+ levelControlsDevice = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, context) =>
+ {
+ var message = new LevelControlStateMessage
+ {
+ Levels = levelControlsDevice.LevelControlPoints.ToDictionary(kv => kv.Key, kv => new Volume { Level = kv.Value.VolumeLevelFeedback.IntValue, Muted = kv.Value.MuteFeedback.BoolValue })
+ };
+
+ PostStatusMessage(message);
+ });
+
+ foreach (var levelControl in levelControlsDevice.LevelControlPoints)
+ {
+ // reassigning here just in case of lambda closure issues
+ var key = levelControl.Key;
+ var control = levelControl.Value;
+
+ AddAction($"/{key}/level", (id, content) =>
+ {
+ var request = content.ToObject>();
+
+ control.SetVolume(request.Value);
+ });
+
+ AddAction($"/{key}/muteToggle", (id, content) =>
+ {
+ control.MuteToggle();
+ });
+
+ AddAction($"/{key}/muteOn", (id, content) => control.MuteOn());
+
+ AddAction($"/{key}/muteOff", (id, content) => control.MuteOff());
+
+ AddAction($"/{key}/volumeUp", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => control.VolumeUp(b)));
+
+ AddAction($"/{key}/volumeDown", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => control.VolumeDown(b)));
+
+ control.VolumeLevelFeedback.OutputChange += (o, a) => PostStatusMessage(JToken.FromObject(new
+ {
+ levelControls = new Dictionary
+ {
+ {key, new Volume{Level = a.IntValue} }
+ }
+ }));
+
+ control.MuteFeedback.OutputChange += (o, a) => PostStatusMessage(JToken.FromObject(new
+ {
+ levelControls = new Dictionary
+ {
+ {key, new Volume{Muted = a.BoolValue} }
+ }
+ }));
+ }
+ }
+ }
+
+ public class LevelControlStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("levelControls")]
+ public Dictionary Levels { get; set; }
+ }
+
+ public class LevelControlRequestMessage
+ {
+ [JsonProperty("key")]
+ public string Key { get; set; }
+
+ [JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)]
+ public ushort? Level { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs
new file mode 100644
index 00000000..2a9669f1
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs
@@ -0,0 +1,168 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.Routing;
+using Serilog.Events;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ ///
+ /// Messenger for devices that implment IMatrixRouting
+ ///
+ public class IMatrixRoutingMessenger : MessengerBase
+ {
+ private readonly IMatrixRouting matrixDevice;
+ public IMatrixRoutingMessenger(string key, string messagePath, IMatrixRouting device) : base(key, messagePath, device as IKeyName)
+ {
+ matrixDevice = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) =>
+ {
+ try
+ {
+ Debug.LogMessage(LogEventLevel.Verbose, "InputCount: {inputCount}, OutputCount: {outputCount}", this, matrixDevice.InputSlots.Count, matrixDevice.OutputSlots.Count);
+ var message = new MatrixStateMessage
+ {
+ Outputs = matrixDevice.OutputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingOutput(kvp.Value)),
+ Inputs = matrixDevice.InputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingInput(kvp.Value)),
+ };
+
+
+ PostStatusMessage(message);
+ }
+ catch (Exception e)
+ {
+ Debug.LogMessage(e, "Exception Getting full status: {@exception}", this, e);
+ }
+ });
+
+ AddAction("/route", (id, content) =>
+ {
+ var request = content.ToObject();
+
+ matrixDevice.Route(request.InputKey, request.OutputKey, request.RouteType);
+ });
+
+ foreach (var output in matrixDevice.OutputSlots)
+ {
+ var key = output.Key;
+ var outputSlot = output.Value;
+
+ outputSlot.OutputSlotChanged += (sender, args) =>
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ outputs = matrixDevice.OutputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingOutput(kvp.Value))
+ }));
+ };
+ }
+
+ foreach (var input in matrixDevice.InputSlots)
+ {
+ var key = input.Key;
+ var inputSlot = input.Value;
+
+ inputSlot.VideoSyncChanged += (sender, args) =>
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ inputs = matrixDevice.InputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingInput(kvp.Value))
+ }));
+ };
+ }
+ }
+ }
+
+ public class MatrixStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("outputs")]
+ public Dictionary Outputs;
+
+ [JsonProperty("inputs")]
+ public Dictionary Inputs;
+ }
+
+ public class RoutingInput
+ {
+ private IRoutingInputSlot _input;
+
+ [JsonProperty("txDeviceKey", NullValueHandling = NullValueHandling.Ignore)]
+ public string TxDeviceKey => _input?.TxDeviceKey;
+
+ [JsonProperty("slotNumber", NullValueHandling = NullValueHandling.Ignore)]
+ public int? SlotNumber => _input?.SlotNumber;
+
+ [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
+ [JsonProperty("supportedSignalTypes", NullValueHandling = NullValueHandling.Ignore)]
+ public eRoutingSignalType? SupportedSignalTypes => _input?.SupportedSignalTypes;
+
+ [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
+ public string Name => _input?.Name;
+
+ [JsonProperty("isOnline", NullValueHandling = NullValueHandling.Ignore)]
+ public bool? IsOnline => _input?.IsOnline.BoolValue;
+
+ [JsonProperty("videoSyncDetected", NullValueHandling = NullValueHandling.Ignore)]
+
+ public bool? VideoSyncDetected => _input?.VideoSyncDetected;
+
+ [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)]
+ public string Key => _input?.Key;
+
+ public RoutingInput(IRoutingInputSlot input)
+ {
+ _input = input;
+ }
+ }
+
+ public class RoutingOutput
+ {
+ private IRoutingOutputSlot _output;
+
+
+ public RoutingOutput(IRoutingOutputSlot output)
+ {
+ _output = output;
+ }
+
+ [JsonProperty("rxDeviceKey")]
+ public string RxDeviceKey => _output.RxDeviceKey;
+
+ [JsonProperty("currentRoutes")]
+ public Dictionary CurrentRoutes => _output.CurrentRoutes.ToDictionary(kvp => kvp.Key.ToString(), kvp => new RoutingInput(kvp.Value));
+
+ [JsonProperty("slotNumber")]
+ public int SlotNumber => _output.SlotNumber;
+
+ [JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
+ [JsonProperty("supportedSignalTypes")]
+ public eRoutingSignalType SupportedSignalTypes => _output.SupportedSignalTypes;
+
+ [JsonProperty("name")]
+ public string Name => _output.Name;
+
+ [JsonProperty("key")]
+ public string Key => _output.Key;
+ }
+
+ public class MatrixRouteRequest
+ {
+ [JsonProperty("outputKey")]
+ public string OutputKey { get; set; }
+
+ [JsonProperty("inputKey")]
+ public string InputKey { get; set; }
+
+ [JsonProperty("routeType")]
+ public eRoutingSignalType RouteType { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs
new file mode 100644
index 00000000..a66a8586
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs
@@ -0,0 +1,78 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+using System;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class IProjectorScreenLiftControlMessenger : MessengerBase
+ {
+ private readonly IProjectorScreenLiftControl device;
+
+ public IProjectorScreenLiftControlMessenger(string key, string messagePath, IProjectorScreenLiftControl screenLiftDevice)
+ : base(key, messagePath, screenLiftDevice as IKeyName)
+ {
+ device = screenLiftDevice;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) => SendFullStatus());
+
+ AddAction("/raise", (id, content) =>
+ {
+
+ device.Raise();
+
+ });
+
+ AddAction("/lower", (id, content) =>
+ {
+
+ device.Lower();
+
+ });
+
+ device.PositionChanged += Device_PositionChanged;
+
+ }
+
+ private void Device_PositionChanged(object sender, EventArgs e)
+ {
+ var state = new
+ {
+ inUpPosition = device.InUpPosition
+ };
+ PostStatusMessage(JToken.FromObject(state));
+ }
+
+ private void SendFullStatus()
+ {
+ var state = new ScreenLiftStateMessage
+ {
+ InUpPosition = device.InUpPosition,
+ Type = device.Type,
+ DisplayDeviceKey = device.DisplayDeviceKey
+ };
+
+ PostStatusMessage(state);
+ }
+ }
+
+ public class ScreenLiftStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("inUpPosition", NullValueHandling = NullValueHandling.Ignore)]
+ public bool? InUpPosition { get; set; }
+
+ [JsonProperty("displayDeviceKey", NullValueHandling = NullValueHandling.Ignore)]
+ public string DisplayDeviceKey { get; set; }
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)]
+ public eScreenLiftControlType Type { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs
new file mode 100644
index 00000000..8ca6668b
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs
@@ -0,0 +1,84 @@
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Core.Logging;
+using PepperDash.Essentials.Core;
+using System;
+
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class RunRouteActionMessenger : MessengerBase
+ {
+ ///
+ /// Device being bridged
+ ///
+ public IRunRouteAction RoutingDevice { get; private set; }
+
+ public RunRouteActionMessenger(string key, IRunRouteAction routingDevice, string messagePath)
+ : base(key, messagePath, routingDevice as IKeyName)
+ {
+ RoutingDevice = routingDevice ?? throw new ArgumentNullException("routingDevice");
+
+
+ if (RoutingDevice is IRoutingSink routingSink)
+ {
+ routingSink.CurrentSourceChange += RoutingSink_CurrentSourceChange;
+ }
+ }
+
+ private void RoutingSink_CurrentSourceChange(SourceListItem info, ChangeType type)
+ {
+ SendRoutingFullMessageObject();
+ }
+
+ protected override void RegisterActions()
+ {
+ AddAction("/fullStatus", (id, content) => SendRoutingFullMessageObject());
+
+ AddAction("/source", (id, content) =>
+ {
+ var c = content.ToObject();
+ // assume no sourceListKey
+ var sourceListKey = string.Empty;
+
+ if (!string.IsNullOrEmpty(c.SourceListKey))
+ {
+ // Check for source list in content of message
+ sourceListKey = c.SourceListKey;
+ }
+
+ RoutingDevice.RunRouteAction(c.SourceListItemKey, sourceListKey);
+ });
+
+ if (RoutingDevice is IRoutingSink sinkDevice)
+ {
+ sinkDevice.CurrentSourceChange += (o, a) => SendRoutingFullMessageObject();
+ }
+ }
+
+ ///
+ /// Helper method to update full status of the routing device
+ ///
+ private void SendRoutingFullMessageObject()
+ {
+ if (RoutingDevice is IRoutingSink sinkDevice)
+ {
+ var sourceKey = sinkDevice.CurrentSourceInfoKey;
+
+ if (string.IsNullOrEmpty(sourceKey))
+ sourceKey = "none";
+
+ PostStatusMessage(new RoutingStateMessage
+ {
+ SelectedSourceKey = sourceKey
+ });
+ }
+ }
+ }
+
+ public class RoutingStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("selectedSourceKey")]
+ public string SelectedSourceKey { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs
new file mode 100644
index 00000000..f66ba53d
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs
@@ -0,0 +1,65 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class ISelectableItemsMessenger : MessengerBase
+ {
+ private static readonly JsonSerializer serializer = new JsonSerializer { Converters = { new StringEnumConverter() } };
+ private readonly ISelectableItems itemDevice;
+
+ private readonly string _propName;
+ public ISelectableItemsMessenger(string key, string messagePath, ISelectableItems device, string propName) : base(key, messagePath, device as IKeyName)
+ {
+ itemDevice = device;
+ _propName = propName;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, context) =>
+ {
+ SendFullStatus();
+ });
+
+ itemDevice.ItemsUpdated += (sender, args) =>
+ {
+ SendFullStatus();
+ };
+
+ itemDevice.CurrentItemChanged += (sender, args) =>
+ {
+ SendFullStatus();
+ };
+
+ foreach (var input in itemDevice.Items)
+ {
+ var key = input.Key;
+ var localItem = input.Value;
+
+ AddAction($"/{key}", (id, content) =>
+ {
+ localItem.Select();
+ });
+
+ localItem.ItemUpdated += (sender, args) =>
+ {
+ SendFullStatus();
+ };
+ }
+ }
+
+ private void SendFullStatus()
+ {
+ var stateObject = new JObject();
+ stateObject[_propName] = JToken.FromObject(itemDevice, serializer);
+ PostStatusMessage(stateObject);
+ }
+ }
+
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs
new file mode 100644
index 00000000..ca5fc3d3
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs
@@ -0,0 +1,93 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class IShutdownPromptTimerMessenger : MessengerBase
+ {
+ private readonly IShutdownPromptTimer _room;
+
+ public IShutdownPromptTimerMessenger(string key, string messagePath, IShutdownPromptTimer room)
+ : base(key, messagePath, room as IKeyName)
+ {
+ _room = room;
+ }
+
+ protected override void RegisterActions()
+ {
+ AddAction("/status", (id, content) =>
+ {
+ SendFullStatus();
+ });
+
+ AddAction("/setShutdownPromptSeconds", (id, content) =>
+ {
+ var response = content.ToObject();
+
+ _room.SetShutdownPromptSeconds(response);
+
+ SendFullStatus();
+ });
+
+ AddAction("/shutdownStart", (id, content) => _room.StartShutdown(eShutdownType.Manual));
+
+ AddAction("/shutdownEnd", (id, content) => _room.ShutdownPromptTimer.Finish());
+
+ AddAction("/shutdownCancel", (id, content) => _room.ShutdownPromptTimer.Cancel());
+
+
+ _room.ShutdownPromptTimer.HasStarted += (sender, args) =>
+ {
+ PostEventMessage("timerStarted");
+ };
+
+ _room.ShutdownPromptTimer.HasFinished += (sender, args) =>
+ {
+ PostEventMessage("timerFinished");
+ };
+
+ _room.ShutdownPromptTimer.WasCancelled += (sender, args) =>
+ {
+ PostEventMessage("timerCancelled");
+ };
+
+ _room.ShutdownPromptTimer.SecondsRemainingFeedback.OutputChange += (sender, args) =>
+ {
+ var status = new
+ {
+ secondsRemaining = _room.ShutdownPromptTimer.SecondsRemainingFeedback.IntValue,
+ percentageRemaining = _room.ShutdownPromptTimer.PercentFeedback.UShortValue
+ };
+
+ PostStatusMessage(JToken.FromObject(status));
+ };
+ }
+
+ private void SendFullStatus()
+ {
+ var status = new IShutdownPromptTimerStateMessage
+ {
+ ShutdownPromptSeconds = _room.ShutdownPromptTimer.SecondsToCount,
+ SecondsRemaining = _room.ShutdownPromptTimer.SecondsRemainingFeedback.IntValue,
+ PercentageRemaining = _room.ShutdownPromptTimer.PercentFeedback.UShortValue
+ };
+
+ PostStatusMessage(status);
+ }
+ }
+
+
+ public class IShutdownPromptTimerStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("secondsRemaining")]
+ public int SecondsRemaining { get; set; }
+
+ [JsonProperty("percentageRemaining")]
+ public int PercentageRemaining { get; set; }
+
+ [JsonProperty("shutdownPromptSeconds")]
+ public int ShutdownPromptSeconds { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs
new file mode 100644
index 00000000..f49d189d
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs
@@ -0,0 +1,58 @@
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Essentials.Core.CrestronIO;
+using System;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class ISwitchedOutputMessenger : MessengerBase
+ {
+
+ private readonly ISwitchedOutput device;
+
+ public ISwitchedOutputMessenger(string key, ISwitchedOutput device, string messagePath)
+ : base(key, messagePath, device as IKeyName)
+ {
+ this.device = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) => SendFullStatus());
+
+ AddAction("/on", (id, content) =>
+ {
+
+ device.On();
+
+ });
+
+ AddAction("/off", (id, content) =>
+ {
+
+ device.Off();
+
+ });
+
+ device.OutputIsOnFeedback.OutputChange += new EventHandler((o, a) => SendFullStatus());
+ }
+
+ private void SendFullStatus()
+ {
+ var state = new ISwitchedOutputStateMessage
+ {
+ IsOn = device.OutputIsOnFeedback.BoolValue
+ };
+
+ PostStatusMessage(state);
+ }
+ }
+
+ public class ISwitchedOutputStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("isOn")]
+ public bool IsOn { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs
new file mode 100644
index 00000000..5b15d7ab
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs
@@ -0,0 +1,88 @@
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class ITechPasswordMessenger : MessengerBase
+ {
+ private readonly ITechPassword _room;
+
+ public ITechPasswordMessenger(string key, string messagePath, ITechPassword room)
+ : base(key, messagePath, room as IKeyName)
+ {
+ _room = room;
+ }
+
+ protected override void RegisterActions()
+ {
+
+ AddAction("/status", (id, content) =>
+ {
+ SendFullStatus();
+ });
+
+ AddAction("/validateTechPassword", (id, content) =>
+ {
+ var password = content.Value("password");
+
+ _room.ValidateTechPassword(password);
+ });
+
+ AddAction("/setTechPassword", (id, content) =>
+ {
+ var response = content.ToObject();
+
+ _room.SetTechPassword(response.OldPassword, response.NewPassword);
+ });
+
+ _room.TechPasswordChanged += (sender, args) =>
+ {
+ PostEventMessage("passwordChangedSuccessfully");
+ };
+
+ _room.TechPasswordValidateResult += (sender, args) =>
+ {
+ var evt = new ITechPasswordEventMessage
+ {
+ IsValid = args.IsValid
+ };
+
+ PostEventMessage(evt, "passwordValidationResult");
+ };
+ }
+
+ private void SendFullStatus()
+ {
+ var status = new ITechPasswordStateMessage
+ {
+ TechPasswordLength = _room.TechPasswordLength
+ };
+
+ PostStatusMessage(status);
+ }
+
+ }
+
+ public class ITechPasswordStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("techPasswordLength", NullValueHandling = NullValueHandling.Ignore)]
+ public int? TechPasswordLength { get; set; }
+ }
+
+ public class ITechPasswordEventMessage : DeviceEventMessageBase
+ {
+ [JsonProperty("isValid", NullValueHandling = NullValueHandling.Ignore)]
+ public bool? IsValid { get; set; }
+ }
+
+ internal class SetTechPasswordContent
+ {
+ [JsonProperty("oldPassword")]
+ public string OldPassword { get; set; }
+
+ [JsonProperty("newPassword")]
+ public string NewPassword { get; set; }
+ }
+
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs
new file mode 100644
index 00000000..6f7371c1
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs
@@ -0,0 +1,61 @@
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+using System;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class ITemperatureSensorMessenger : MessengerBase
+ {
+ private readonly ITemperatureSensor device;
+
+ public ITemperatureSensorMessenger(string key, ITemperatureSensor device, string messagePath)
+ : base(key, messagePath, device as IKeyName)
+ {
+ this.device = device;
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) => SendFullStatus());
+
+ AddAction("/setTemperatureUnitsToCelcius", (id, content) =>
+ {
+ device.SetTemperatureFormat(true);
+ });
+
+ AddAction("/setTemperatureUnitsToFahrenheit", (id, content) =>
+ {
+ device.SetTemperatureFormat(false);
+ });
+
+ device.TemperatureFeedback.OutputChange += new EventHandler((o, a) => SendFullStatus());
+ device.TemperatureInCFeedback.OutputChange += new EventHandler((o, a) => SendFullStatus());
+ }
+
+ private void SendFullStatus()
+ {
+ // format the temperature to a string with one decimal place
+ var tempString = string.Format("{0}.{1}", device.TemperatureFeedback.UShortValue / 10, device.TemperatureFeedback.UShortValue % 10);
+
+ var state = new ITemperatureSensorStateMessage
+ {
+ Temperature = tempString,
+ TemperatureInCelsius = device.TemperatureInCFeedback.BoolValue
+ };
+
+ PostStatusMessage(state);
+ }
+ }
+
+ public class ITemperatureSensorStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("temperature")]
+ public string Temperature { get; set; }
+
+ [JsonProperty("temperatureInCelsius")]
+ public bool TemperatureInCelsius { get; set; }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs
new file mode 100644
index 00000000..c8973aae
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs
@@ -0,0 +1,66 @@
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Essentials.Core.Lighting;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class ILightingScenesMessenger : MessengerBase
+ {
+ protected ILightingScenes Device { get; private set; }
+
+ public ILightingScenesMessenger(string key, ILightingScenes device, string messagePath)
+ : base(key, messagePath, device as IKeyName)
+ {
+ Device = device ?? throw new ArgumentNullException("device");
+ Device.LightingSceneChange += new EventHandler(LightingDevice_LightingSceneChange);
+
+
+ }
+
+ private void LightingDevice_LightingSceneChange(object sender, LightingSceneChangeEventArgs e)
+ {
+ var state = new LightingBaseStateMessage
+ {
+ CurrentLightingScene = e.CurrentLightingScene
+ };
+
+ PostStatusMessage(state);
+ }
+
+ protected override void RegisterActions()
+ {
+ base.RegisterActions();
+
+ AddAction("/fullStatus", (id, content) => SendFullStatus());
+
+ AddAction("/selectScene", (id, content) =>
+ {
+ var s = content.ToObject();
+ Device.SelectScene(s);
+ });
+ }
+
+
+ private void SendFullStatus()
+ {
+ var state = new LightingBaseStateMessage
+ {
+ Scenes = Device.LightingScenes,
+ CurrentLightingScene = Device.CurrentLightingScene
+ };
+
+ PostStatusMessage(state);
+ }
+ }
+
+ public class LightingBaseStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("scenes", NullValueHandling = NullValueHandling.Ignore)]
+ public List Scenes { get; set; }
+
+ [JsonProperty("currentLightingScene", NullValueHandling = NullValueHandling.Ignore)]
+ public LightingScene CurrentLightingScene { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs
new file mode 100644
index 00000000..7d809f13
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs
@@ -0,0 +1,288 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ ///
+ /// Provides a messaging bridge
+ ///
+ public abstract class MessengerBase : EssentialsDevice, IMobileControlMessenger
+ {
+ protected IKeyName _device;
+
+ private readonly List _deviceInterfaces;
+
+ private readonly Dictionary> _actions = new Dictionary>();
+
+ public string DeviceKey => _device?.Key ?? "";
+
+ ///
+ ///
+ ///
+
+ public IMobileControl AppServerController { get; private set; }
+
+ public string MessagePath { get; private set; }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected MessengerBase(string key, string messagePath)
+ : base(key)
+ {
+ Key = key;
+
+ if (string.IsNullOrEmpty(messagePath))
+ throw new ArgumentException("messagePath must not be empty or null");
+
+ MessagePath = messagePath;
+ }
+
+ protected MessengerBase(string key, string messagePath, IKeyName device)
+ : this(key, messagePath)
+ {
+ _device = device;
+
+ _deviceInterfaces = GetInterfaces(_device as Device);
+ }
+
+ ///
+ /// Gets the interfaces implmented on the device
+ ///
+ ///
+ ///
+ private List GetInterfaces(Device device)
+ {
+ return device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List();
+ }
+
+ ///
+ /// Registers this messenger with appserver controller
+ ///
+ ///
+ public void RegisterWithAppServer(IMobileControl appServerController)
+ {
+ AppServerController = appServerController ?? throw new ArgumentNullException("appServerController");
+
+ AppServerController.AddAction(this, HandleMessage);
+
+ RegisterActions();
+ }
+
+ private void HandleMessage(string path, string id, JToken content)
+ {
+ // replace base path with empty string. Should leave something like /fullStatus
+ var route = path.Replace(MessagePath, string.Empty);
+
+ if (!_actions.TryGetValue(route, out var action))
+ {
+ return;
+ }
+
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Executing action for path {path}", this, path);
+
+ action(id, content);
+ }
+
+ protected void AddAction(string path, Action action)
+ {
+ if (_actions.ContainsKey(path))
+ {
+ //Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Messenger {Key} already has action registered at {path}", this);
+ return;
+ }
+
+ _actions.Add(path, action);
+ }
+
+ public List GetActionPaths()
+ {
+ return _actions.Keys.ToList();
+ }
+
+ protected void RemoveAction(string path)
+ {
+ if (!_actions.ContainsKey(path))
+ {
+ return;
+ }
+
+ _actions.Remove(path);
+ }
+
+ ///
+ /// Implemented in extending classes. Wire up API calls and feedback here
+ ///
+ ///
+ protected virtual void RegisterActions()
+ {
+
+ }
+
+ ///
+ /// Helper for posting status message
+ ///
+ ///
+ ///
+ protected void PostStatusMessage(DeviceStateMessageBase message, string clientId = null)
+ {
+ try
+ {
+ if (message == null)
+ {
+ throw new ArgumentNullException("message");
+ }
+
+ if (_device == null)
+ {
+ throw new ArgumentNullException("device");
+ }
+
+ message.SetInterfaces(_deviceInterfaces);
+
+ message.Key = _device.Key;
+
+ message.Name = _device.Name;
+
+ PostStatusMessage(JToken.FromObject(message), MessagePath, clientId);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Exception posting status message", this);
+ }
+ }
+
+ protected void PostStatusMessage(string type, DeviceStateMessageBase deviceState, string clientId = null)
+ {
+ try
+ {
+ //Debug.Console(2, this, "*********************Setting DeviceStateMessageProperties on MobileControlResponseMessage");
+ deviceState.SetInterfaces(_deviceInterfaces);
+
+ deviceState.Key = _device.Key;
+
+ deviceState.Name = _device.Name;
+
+ deviceState.MessageBasePath = MessagePath;
+
+ PostStatusMessage(JToken.FromObject(deviceState), type, clientId);
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Exception posting status message", this);
+ }
+ }
+
+ protected void PostStatusMessage(JToken content, string type = "", string clientId = null)
+ {
+ try
+ {
+ AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = clientId, Content = content });
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(ex, "Exception posting status message", this);
+ }
+ }
+
+ protected void PostEventMessage(DeviceEventMessageBase message)
+ {
+ message.Key = _device.Key;
+
+ message.Name = _device.Name;
+
+ AppServerController?.SendMessageObject(new MobileControlMessage
+ {
+ Type = $"/event{MessagePath}/{message.EventType}",
+ Content = JToken.FromObject(message),
+ });
+ }
+
+ protected void PostEventMessage(DeviceEventMessageBase message, string eventType)
+ {
+ message.Key = _device.Key;
+
+ message.Name = _device.Name;
+
+ message.EventType = eventType;
+
+ AppServerController?.SendMessageObject(new MobileControlMessage
+ {
+ Type = $"/event{MessagePath}/{eventType}",
+ Content = JToken.FromObject(message),
+ });
+ }
+
+ protected void PostEventMessage(string eventType)
+ {
+ AppServerController?.SendMessageObject(new MobileControlMessage
+ {
+ Type = $"/event{MessagePath}/{eventType}",
+ Content = JToken.FromObject(new { }),
+ });
+ }
+
+ }
+
+ public abstract class DeviceMessageBase
+ {
+ ///
+ /// The device key
+ ///
+ [JsonProperty("key")]
+ public string Key { get; set; }
+
+ ///
+ /// The device name
+ ///
+ [JsonProperty("name")]
+ public string Name { get; set; }
+
+ ///
+ /// The type of the message class
+ ///
+ [JsonProperty("messageType")]
+ public string MessageType => GetType().Name;
+
+ [JsonProperty("messageBasePath")]
+ public string MessageBasePath { get; set; }
+ }
+
+ ///
+ /// Base class for state messages that includes the type of message and the implmented interfaces
+ ///
+ public class DeviceStateMessageBase : DeviceMessageBase
+ {
+ ///
+ /// The interfaces implmented by the device sending the messsage
+ ///
+ [JsonProperty("interfaces")]
+ public List Interfaces { get; private set; }
+
+ public void SetInterfaces(List interfaces)
+ {
+ Interfaces = interfaces;
+ }
+ }
+
+ ///
+ /// Base class for event messages that include the type of message and an event type
+ ///
+ public abstract class DeviceEventMessageBase : DeviceMessageBase
+ {
+ ///
+ /// The event type
+ ///
+ [JsonProperty("eventType")]
+ public string EventType { get; set; }
+ }
+
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs
new file mode 100644
index 00000000..9ec2a4e4
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs
@@ -0,0 +1,116 @@
+using Crestron.SimplSharp;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public static class PressAndHoldHandler
+ {
+ private const long ButtonHeartbeatInterval = 1000;
+
+ private static readonly Dictionary _pushedActions = new Dictionary();
+
+ private static readonly Dictionary>> _pushedActionHandlers;
+
+ static PressAndHoldHandler()
+ {
+ _pushedActionHandlers = new Dictionary>>
+ {
+ {"pressed", AddTimer },
+ {"held", ResetTimer },
+ {"released", StopTimer }
+ };
+ }
+
+ private static void AddTimer(string deviceKey, Action action)
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Attempting to add timer for {deviceKey}", deviceKey);
+
+ if (_pushedActions.TryGetValue(deviceKey, out CTimer cancelTimer))
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Timer for {deviceKey} already exists", deviceKey);
+ return;
+ }
+
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Adding timer for {deviceKey} with due time {dueTime}", deviceKey, ButtonHeartbeatInterval);
+
+ action(true);
+
+ cancelTimer = new CTimer(o =>
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Timer expired for {deviceKey}", deviceKey);
+
+ action(false);
+
+ _pushedActions.Remove(deviceKey);
+ }, ButtonHeartbeatInterval);
+
+ _pushedActions.Add(deviceKey, cancelTimer);
+ }
+
+ private static void ResetTimer(string deviceKey, Action action)
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Attempting to reset timer for {deviceKey}", deviceKey);
+
+ if (!_pushedActions.TryGetValue(deviceKey, out CTimer cancelTimer))
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Timer for {deviceKey} not found", deviceKey);
+ return;
+ }
+
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Resetting timer for {deviceKey} with due time {dueTime}", deviceKey, ButtonHeartbeatInterval);
+
+ cancelTimer.Reset(ButtonHeartbeatInterval);
+ }
+
+ private static void StopTimer(string deviceKey, Action action)
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Attempting to stop timer for {deviceKey}", deviceKey);
+
+ if (!_pushedActions.TryGetValue(deviceKey, out CTimer cancelTimer))
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Timer for {deviceKey} not found", deviceKey);
+ return;
+ }
+
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Stopping timer for {deviceKey} with due time {dueTime}", deviceKey, ButtonHeartbeatInterval);
+
+ action(false);
+ cancelTimer.Stop();
+ _pushedActions.Remove(deviceKey);
+ }
+
+ public static Action> GetPressAndHoldHandler(string value)
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Getting press and hold handler for {value}", value);
+
+ if (!_pushedActionHandlers.TryGetValue(value, out Action> handler))
+ {
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Press and hold handler for {value} not found", value);
+ return null;
+ }
+
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Got handler for {value}", value);
+
+ return handler;
+ }
+
+ public static void HandlePressAndHold(string deviceKey, JToken content, Action action)
+ {
+ var msg = content.ToObject>();
+
+ Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Handling press and hold message of {type} for {deviceKey}", msg.Value, deviceKey);
+
+ var timerHandler = GetPressAndHoldHandler(msg.Value);
+
+ if (timerHandler == null)
+ {
+ return;
+ }
+
+ timerHandler(deviceKey, action);
+ }
+ }
+}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs
new file mode 100644
index 00000000..b8f5feff
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs
@@ -0,0 +1,76 @@
+using Newtonsoft.Json;
+using PepperDash.Core;
+using PepperDash.Core.Logging;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Room.Config;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class RoomEventScheduleMessenger : MessengerBase
+ {
+ private readonly IRoomEventSchedule _room;
+
+
+ public RoomEventScheduleMessenger(string key, string messagePath, IRoomEventSchedule room)
+ : base(key, messagePath, room as IKeyName)
+ {
+ _room = room;
+ }
+
+ #region Overrides of MessengerBase
+
+ protected override void RegisterActions()
+ {
+ AddAction("/saveScheduledEvents", (id, content) => SaveScheduledEvents(content.ToObject>()));
+ AddAction("/status", (id, content) =>
+ {
+ var events = _room.GetScheduledEvents();
+
+ SendFullStatus(events);
+ });
+
+ _room.ScheduledEventsChanged += (sender, args) => SendFullStatus(args.ScheduledEvents);
+ }
+
+ #endregion
+
+ private void SaveScheduledEvents(List events)
+ {
+ foreach (var evt in events)
+ {
+ SaveScheduledEvent(evt);
+ }
+ }
+
+ private void SaveScheduledEvent(ScheduledEventConfig eventConfig)
+ {
+ try
+ {
+ _room.AddOrUpdateScheduledEvent(eventConfig);
+ }
+ catch (Exception ex)
+ {
+ this.LogException(ex,"Exception saving event");
+ }
+ }
+
+ private void SendFullStatus(List events)
+ {
+
+ var message = new RoomEventScheduleStateMessage
+ {
+ ScheduleEvents = events,
+ };
+
+ PostStatusMessage(message);
+ }
+ }
+
+ public class RoomEventScheduleStateMessage : DeviceStateMessageBase
+ {
+ [JsonProperty("scheduleEvents")]
+ public List ScheduleEvents { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs
new file mode 100644
index 00000000..3222289a
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs
@@ -0,0 +1,158 @@
+using Crestron.SimplSharpPro.DeviceSupport;
+using Newtonsoft.Json.Linq;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Devices.Common.Codec;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ // ReSharper disable once InconsistentNaming
+ public class SIMPLAtcMessenger : MessengerBase
+ {
+ private readonly BasicTriList _eisc;
+
+ public SIMPLAtcJoinMap JoinMap { get; private set; }
+
+
+ ///
+ ///
+ ///
+ private readonly CodecActiveCallItem _currentCallItem;
+
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public SIMPLAtcMessenger(string key, BasicTriList eisc, string messagePath)
+ : base(key, messagePath)
+ {
+ _eisc = eisc;
+
+ JoinMap = new SIMPLAtcJoinMap(201);
+
+ _currentCallItem = new CodecActiveCallItem { Type = eCodecCallType.Audio, Id = "-audio-" };
+ }
+
+ ///
+ ///
+ ///
+ private void SendFullStatus()
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ calls = GetCurrentCallList(),
+ currentCallString = _eisc.GetString(JoinMap.CurrentCallName.JoinNumber),
+ currentDialString = _eisc.GetString(JoinMap.CurrentDialString.JoinNumber),
+ isInCall = _eisc.GetString(JoinMap.HookState.JoinNumber) == "Connected"
+ })
+ );
+ }
+
+ ///
+ ///
+ ///
+ ///
+ protected override void RegisterActions()
+ {
+ //EISC.SetStringSigAction(SCurrentDialString, s => PostStatusMessage(new { currentDialString = s }));
+
+ _eisc.SetStringSigAction(JoinMap.HookState.JoinNumber, s =>
+ {
+ _currentCallItem.Status = (eCodecCallStatus)Enum.Parse(typeof(eCodecCallStatus), s, true);
+ //GetCurrentCallList();
+ SendFullStatus();
+ });
+
+ _eisc.SetStringSigAction(JoinMap.CurrentCallNumber.JoinNumber, s =>
+ {
+ _currentCallItem.Number = s;
+ SendCallsList();
+ });
+
+ _eisc.SetStringSigAction(JoinMap.CurrentCallName.JoinNumber, s =>
+ {
+ _currentCallItem.Name = s;
+ SendCallsList();
+ });
+
+ _eisc.SetStringSigAction(JoinMap.CallDirection.JoinNumber, s =>
+ {
+ _currentCallItem.Direction = (eCodecCallDirection)Enum.Parse(typeof(eCodecCallDirection), s, true);
+ SendCallsList();
+ });
+
+ // Add press and holds using helper
+ //Action addPhAction = (s, u) =>
+ // AppServerController.AddAction(MessagePath + s, new PressAndHoldAction(b => _eisc.SetBool(u, b)));
+
+ // Add straight pulse calls
+ void addAction(string s, uint u) =>
+ AddAction(s, (id, content) => _eisc.PulseBool(u, 100));
+ addAction("/endCallById", JoinMap.EndCall.JoinNumber);
+ addAction("/endAllCalls", JoinMap.EndCall.JoinNumber);
+ addAction("/acceptById", JoinMap.IncomingAnswer.JoinNumber);
+ addAction("/rejectById", JoinMap.IncomingReject.JoinNumber);
+
+ var speeddialStart = JoinMap.SpeedDialStart.JoinNumber;
+ var speeddialEnd = JoinMap.SpeedDialStart.JoinNumber + JoinMap.SpeedDialStart.JoinSpan;
+
+ var speedDialIndex = 1;
+ for (uint i = speeddialStart; i < speeddialEnd; i++)
+ {
+ addAction(string.Format("/speedDial{0}", speedDialIndex), i);
+ speedDialIndex++;
+ }
+
+ // Get status
+ AddAction("/fullStatus", (id, content) => SendFullStatus());
+ // Dial on string
+ AddAction("/dial",
+ (id, content) =>
+ {
+ var msg = content.ToObject>();
+ _eisc.SetString(JoinMap.CurrentDialString.JoinNumber, msg.Value);
+ });
+ // Pulse DTMF
+ AddAction("/dtmf", (id, content) =>
+ {
+ var s = content.ToObject>();
+
+ var join = JoinMap.Joins[s.Value];
+ if (join != null)
+ {
+ if (join.JoinNumber > 0)
+ {
+ _eisc.PulseBool(join.JoinNumber, 100);
+ }
+ }
+ });
+ }
+
+ ///
+ ///
+ ///
+ private void SendCallsList()
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ calls = GetCurrentCallList(),
+ })
+ );
+ }
+
+ ///
+ /// Turns the
+ ///
+ ///
+ private List GetCurrentCallList()
+ {
+ return _currentCallItem.Status == eCodecCallStatus.Disconnected
+ ? new List()
+ : new List { _currentCallItem };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs
new file mode 100644
index 00000000..bf4eb765
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs
@@ -0,0 +1,161 @@
+using Crestron.SimplSharpPro.DeviceSupport;
+using Newtonsoft.Json.Linq;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.Bridges;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+using PepperDash.Essentials.Devices.Common.Cameras;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ // ReSharper disable once InconsistentNaming
+ public class SIMPLCameraMessenger : MessengerBase
+ {
+ private readonly BasicTriList _eisc;
+
+ private readonly CameraControllerJoinMap _joinMap;
+
+
+ public SIMPLCameraMessenger(string key, BasicTriList eisc, string messagePath, uint joinStart)
+ : base(key, messagePath)
+ {
+ _eisc = eisc;
+
+ _joinMap = new CameraControllerJoinMap(joinStart);
+
+ _eisc.SetUShortSigAction(_joinMap.NumberOfPresets.JoinNumber, u => SendCameraFullMessageObject());
+
+ _eisc.SetBoolSigAction(_joinMap.CameraModeAuto.JoinNumber, b => PostCameraMode());
+ _eisc.SetBoolSigAction(_joinMap.CameraModeManual.JoinNumber, b => PostCameraMode());
+ _eisc.SetBoolSigAction(_joinMap.CameraModeOff.JoinNumber, b => PostCameraMode());
+ }
+
+
+ protected override void RegisterActions()
+ {
+ AddAction("/fullStatus", (id, content) => SendCameraFullMessageObject());
+
+ // Add press and holds using helper action
+ void addPhAction(string s, uint u) =>
+ AddAction(s, (id, content) => HandleCameraPressAndHold(content, b => _eisc.SetBool(u, b)));
+ addPhAction("/cameraUp", _joinMap.TiltUp.JoinNumber);
+ addPhAction("/cameraDown", _joinMap.TiltDown.JoinNumber);
+ addPhAction("/cameraLeft", _joinMap.PanLeft.JoinNumber);
+ addPhAction("/cameraRight", _joinMap.PanRight.JoinNumber);
+ addPhAction("/cameraZoomIn", _joinMap.ZoomIn.JoinNumber);
+ addPhAction("/cameraZoomOut", _joinMap.ZoomOut.JoinNumber);
+
+ void addAction(string s, uint u) =>
+ AddAction(s, (id, content) => _eisc.PulseBool(u, 100));
+
+ addAction("/cameraModeAuto", _joinMap.CameraModeAuto.JoinNumber);
+ addAction("/cameraModeManual", _joinMap.CameraModeManual.JoinNumber);
+ addAction("/cameraModeOff", _joinMap.CameraModeOff.JoinNumber);
+
+ var presetStart = _joinMap.PresetRecallStart.JoinNumber;
+ var presetEnd = _joinMap.PresetRecallStart.JoinNumber + _joinMap.PresetRecallStart.JoinSpan;
+
+ int presetId = 1;
+ // camera presets
+ for (uint i = presetStart; i <= presetEnd; i++)
+ {
+ addAction("/cameraPreset" + (presetId), i);
+ presetId++;
+ }
+ }
+
+ private void HandleCameraPressAndHold(JToken content, Action cameraAction)
+ {
+ var state = content.ToObject>();
+
+ var timerHandler = PressAndHoldHandler.GetPressAndHoldHandler(state.Value);
+ if (timerHandler == null)
+ {
+ return;
+ }
+
+ timerHandler(state.Value, cameraAction);
+
+ cameraAction(state.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ public void CustomUnregisterWithAppServer(IMobileControl appServerController)
+ {
+ appServerController.RemoveAction(MessagePath + "/fullStatus");
+
+ appServerController.RemoveAction(MessagePath + "/cameraUp");
+ appServerController.RemoveAction(MessagePath + "/cameraDown");
+ appServerController.RemoveAction(MessagePath + "/cameraLeft");
+ appServerController.RemoveAction(MessagePath + "/cameraRight");
+ appServerController.RemoveAction(MessagePath + "/cameraZoomIn");
+ appServerController.RemoveAction(MessagePath + "/cameraZoomOut");
+ appServerController.RemoveAction(MessagePath + "/cameraModeAuto");
+ appServerController.RemoveAction(MessagePath + "/cameraModeManual");
+ appServerController.RemoveAction(MessagePath + "/cameraModeOff");
+
+ _eisc.SetUShortSigAction(_joinMap.NumberOfPresets.JoinNumber, null);
+
+ _eisc.SetBoolSigAction(_joinMap.CameraModeAuto.JoinNumber, null);
+ _eisc.SetBoolSigAction(_joinMap.CameraModeManual.JoinNumber, null);
+ _eisc.SetBoolSigAction(_joinMap.CameraModeOff.JoinNumber, null);
+ }
+
+ ///
+ /// Helper method to update the full status of the camera
+ ///
+ private void SendCameraFullMessageObject()
+ {
+ var presetList = new List();
+
+ // Build a list of camera presets based on the names and count
+ if (_eisc.GetBool(_joinMap.SupportsPresets.JoinNumber))
+ {
+ var presetStart = _joinMap.PresetLabelStart.JoinNumber;
+ var presetEnd = _joinMap.PresetLabelStart.JoinNumber + _joinMap.NumberOfPresets.JoinNumber;
+
+ var presetId = 1;
+ for (uint i = presetStart; i < presetEnd; i++)
+ {
+ var presetName = _eisc.GetString(i);
+ var preset = new CameraPreset(presetId, presetName, string.IsNullOrEmpty(presetName), true);
+ presetList.Add(preset);
+ presetId++;
+ }
+ }
+
+ PostStatusMessage(JToken.FromObject(new
+ {
+ cameraMode = GetCameraMode(),
+ hasPresets = _eisc.GetBool(_joinMap.SupportsPresets.JoinNumber),
+ presets = presetList
+ })
+ );
+ }
+
+ ///
+ ///
+ ///
+ private void PostCameraMode()
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ cameraMode = GetCameraMode()
+ }));
+ }
+
+ ///
+ /// Computes the current camera mode
+ ///
+ ///
+ private string GetCameraMode()
+ {
+ string m;
+ if (_eisc.GetBool(_joinMap.CameraModeAuto.JoinNumber)) m = eCameraControlMode.Auto.ToString().ToLower();
+ else if (_eisc.GetBool(_joinMap.CameraModeManual.JoinNumber))
+ m = eCameraControlMode.Manual.ToString().ToLower();
+ else m = eCameraControlMode.Off.ToString().ToLower();
+ return m;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs
new file mode 100644
index 00000000..b0ddc47a
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs
@@ -0,0 +1,127 @@
+using Crestron.SimplSharpPro.DeviceSupport;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ public class SimplDirectRouteMessenger : MessengerBase
+ {
+ private readonly BasicTriList _eisc;
+
+ public MobileControlSIMPLRunDirectRouteActionJoinMap JoinMap { get; private set; }
+
+ public Dictionary DestinationList { get; set; }
+
+ public SimplDirectRouteMessenger(string key, BasicTriList eisc, string messagePath) : base(key, messagePath)
+ {
+ _eisc = eisc;
+
+ JoinMap = new MobileControlSIMPLRunDirectRouteActionJoinMap(851);
+
+ DestinationList = new Dictionary();
+ }
+
+ #region Overrides of MessengerBase
+
+ protected override void RegisterActions()
+ {
+ Debug.Console(2, "********** Direct Route Messenger CustomRegisterWithAppServer **********");
+
+
+ //Audio source
+ _eisc.SetStringSigAction(JoinMap.SourceForDestinationAudio.JoinNumber,
+ s => PostStatusMessage(JToken.FromObject(new
+ {
+ selectedSourceKey = s,
+ })
+ ));
+
+ AddAction("/programAudio/selectSource", (id, content) =>
+ {
+ var msg = content.ToObject>();
+
+ _eisc.StringInput[JoinMap.SourceForDestinationAudio.JoinNumber].StringValue = msg.Value;
+ });
+
+ AddAction("/fullStatus", (id, content) =>
+ {
+ foreach (var dest in DestinationList)
+ {
+ var key = dest.Key;
+ var item = dest.Value;
+
+ var source =
+ _eisc.StringOutput[(uint)(JoinMap.SourceForDestinationJoinStart.JoinNumber + item.Order)].StringValue;
+
+ UpdateSourceForDestination(source, key);
+ }
+
+ PostStatusMessage(JToken.FromObject(new
+ {
+ selectedSourceKey = _eisc.StringOutput[JoinMap.SourceForDestinationAudio.JoinNumber].StringValue
+ })
+ );
+
+ PostStatusMessage(JToken.FromObject(new
+ {
+ advancedSharingActive = _eisc.BooleanOutput[JoinMap.AdvancedSharingModeFb.JoinNumber].BoolValue
+ })
+ );
+ });
+
+ AddAction("/advancedSharingMode", (id, content) =>
+ {
+ var b = content.ToObject>();
+
+ Debug.Console(1, "Current Sharing Mode: {2}\r\nadvanced sharing mode: {0} join number: {1}", b.Value,
+ JoinMap.AdvancedSharingModeOn.JoinNumber,
+ _eisc.BooleanOutput[JoinMap.AdvancedSharingModeOn.JoinNumber].BoolValue);
+
+ _eisc.SetBool(JoinMap.AdvancedSharingModeOn.JoinNumber, b.Value);
+ _eisc.SetBool(JoinMap.AdvancedSharingModeOff.JoinNumber, !b.Value);
+ _eisc.PulseBool(JoinMap.AdvancedSharingModeToggle.JoinNumber);
+ });
+
+ _eisc.SetBoolSigAction(JoinMap.AdvancedSharingModeFb.JoinNumber,
+ (b) => PostStatusMessage(JToken.FromObject(new
+ {
+ advancedSharingActive = b
+ })
+ ));
+ }
+
+ public void RegisterForDestinationPaths()
+ {
+ //handle routing feedback from SIMPL
+ foreach (var destination in DestinationList)
+ {
+ var key = destination.Key;
+ var dest = destination.Value;
+
+ _eisc.SetStringSigAction((uint)(JoinMap.SourceForDestinationJoinStart.JoinNumber + dest.Order),
+ s => UpdateSourceForDestination(s, key));
+
+ AddAction($"/{key}/selectSource", (id, content) =>
+ {
+ var s = content.ToObject>();
+
+ _eisc.StringInput[(uint)(JoinMap.SourceForDestinationJoinStart.JoinNumber + dest.Order)].StringValue = s.Value;
+ });
+ }
+ }
+
+ #endregion
+
+ private void UpdateSourceForDestination(string sourceKey, string destKey)
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ selectedSourceKey = sourceKey
+ }), $"{MessagePath}/{destKey}/currentSource");
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs
new file mode 100644
index 00000000..d6d59cc0
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs
@@ -0,0 +1,69 @@
+using Crestron.SimplSharpPro.DeviceSupport;
+using Newtonsoft.Json.Linq;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+
+ public class SIMPLRouteMessenger : MessengerBase
+ {
+ private readonly BasicTriList _eisc;
+
+ private readonly uint _joinStart;
+
+ public class StringJoin
+ {
+ ///
+ /// 1
+ ///
+ public const uint CurrentSource = 1;
+ }
+
+ public SIMPLRouteMessenger(string key, BasicTriList eisc, string messagePath, uint joinStart)
+ : base(key, messagePath)
+ {
+ _eisc = eisc;
+ _joinStart = joinStart - 1;
+
+ _eisc.SetStringSigAction(_joinStart + StringJoin.CurrentSource, SendRoutingFullMessageObject);
+ }
+
+ protected override void RegisterActions()
+ {
+ AddAction("/fullStatus",
+ (id, content) => SendRoutingFullMessageObject(_eisc.GetString(_joinStart + StringJoin.CurrentSource)));
+
+ AddAction("/source", (id, content) =>
+ {
+ var c = content.ToObject();
+
+ _eisc.SetString(_joinStart + StringJoin.CurrentSource, c.SourceListItemKey);
+ });
+ }
+
+ public void CustomUnregisterWithAppServer(IMobileControl appServerController)
+ {
+ appServerController.RemoveAction(MessagePath + "/fullStatus");
+ appServerController.RemoveAction(MessagePath + "/source");
+
+ _eisc.SetStringSigAction(_joinStart + StringJoin.CurrentSource, null);
+ }
+
+ ///
+ /// Helper method to update full status of the routing device
+ ///
+ private void SendRoutingFullMessageObject(string sourceKey)
+ {
+ if (string.IsNullOrEmpty(sourceKey))
+ sourceKey = "none";
+
+ PostStatusMessage(JToken.FromObject(new
+ {
+ selectedSourceKey = sourceKey
+ })
+ );
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs
new file mode 100644
index 00000000..a9872285
--- /dev/null
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs
@@ -0,0 +1,475 @@
+using Crestron.SimplSharpPro.DeviceSupport;
+using Newtonsoft.Json.Linq;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Devices.Common.Cameras;
+using PepperDash.Essentials.Devices.Common.Codec;
+using System;
+using System.Collections.Generic;
+
+namespace PepperDash.Essentials.AppServer.Messengers
+{
+ // ReSharper disable once InconsistentNaming
+ public class SIMPLVtcMessenger : MessengerBase
+ {
+ private readonly BasicTriList _eisc;
+
+ public SIMPLVtcJoinMap JoinMap { get; private set; }
+
+ private readonly CodecActiveCallItem _currentCallItem;
+
+ private CodecActiveCallItem _incomingCallItem;
+
+ private ushort _previousDirectoryLength = 701;
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public SIMPLVtcMessenger(string key, BasicTriList eisc, string messagePath)
+ : base(key, messagePath)
+ {
+ _eisc = eisc;
+
+ JoinMap = new SIMPLVtcJoinMap(1001);
+
+ _currentCallItem = new CodecActiveCallItem { Type = eCodecCallType.Video, Id = "-video-" };
+ }
+
+ ///
+ ///
+ ///
+ ///
+ protected override void RegisterActions()
+ {
+ _eisc.SetStringSigAction(JoinMap.HookState.JoinNumber, s =>
+ {
+ _currentCallItem.Status = (eCodecCallStatus)Enum.Parse(typeof(eCodecCallStatus), s, true);
+ PostFullStatus(); // SendCallsList();
+ });
+
+ _eisc.SetStringSigAction(JoinMap.CurrentCallNumber.JoinNumber, s =>
+ {
+ _currentCallItem.Number = s;
+ PostCallsList();
+ });
+
+ _eisc.SetStringSigAction(JoinMap.CurrentCallName.JoinNumber, s =>
+ {
+ _currentCallItem.Name = s;
+ PostCallsList();
+ });
+
+ _eisc.SetStringSigAction(JoinMap.CallDirection.JoinNumber, s =>
+ {
+ _currentCallItem.Direction = (eCodecCallDirection)Enum.Parse(typeof(eCodecCallDirection), s, true);
+ PostCallsList();
+ });
+
+ _eisc.SetBoolSigAction(JoinMap.IncomingCall.JoinNumber, b =>
+ {
+ if (b)
+ {
+ var ica = new CodecActiveCallItem
+ {
+ Direction = eCodecCallDirection.Incoming,
+ Id = "-video-incoming",
+ Name = _eisc.GetString(JoinMap.IncomingCallName.JoinNumber),
+ Number = _eisc.GetString(JoinMap.IncomingCallNumber.JoinNumber),
+ Status = eCodecCallStatus.Ringing,
+ Type = eCodecCallType.Video
+ };
+ _incomingCallItem = ica;
+ }
+ else
+ {
+ _incomingCallItem = null;
+ }
+ PostCallsList();
+ });
+
+ _eisc.SetStringSigAction(JoinMap.IncomingCallName.JoinNumber, s =>
+ {
+ if (_incomingCallItem != null)
+ {
+ _incomingCallItem.Name = s;
+ PostCallsList();
+ }
+ });
+
+ _eisc.SetStringSigAction(JoinMap.IncomingCallNumber.JoinNumber, s =>
+ {
+ if (_incomingCallItem != null)
+ {
+ _incomingCallItem.Number = s;
+ PostCallsList();
+ }
+ });
+
+ _eisc.SetBoolSigAction(JoinMap.CameraSupportsAutoMode.JoinNumber, b => PostStatusMessage(JToken.FromObject(new
+ {
+ cameraSupportsAutoMode = b
+ })));
+ _eisc.SetBoolSigAction(JoinMap.CameraSupportsOffMode.JoinNumber, b => PostStatusMessage(JToken.FromObject(new
+ {
+ cameraSupportsOffMode = b
+ })));
+
+ // Directory insanity
+ _eisc.SetUShortSigAction(JoinMap.DirectoryRowCount.JoinNumber, u =>
+ {
+ // The length of the list comes in before the list does.
+ // Splice the sig change operation onto the last string sig that will be changing
+ // when the directory entries make it through.
+ if (_previousDirectoryLength > 0)
+ {
+ _eisc.ClearStringSigAction(JoinMap.DirectoryEntriesStart.JoinNumber + _previousDirectoryLength - 1);
+ }
+ _eisc.SetStringSigAction(JoinMap.DirectoryEntriesStart.JoinNumber + u - 1, s => PostDirectory());
+ _previousDirectoryLength = u;
+ });
+
+ _eisc.SetStringSigAction(JoinMap.DirectoryEntrySelectedName.JoinNumber, s => PostStatusMessage(JToken.FromObject(new
+ {
+ directoryContactSelected = new
+ {
+ name = _eisc.GetString(JoinMap.DirectoryEntrySelectedName.JoinNumber),
+ }
+ })));
+
+ _eisc.SetStringSigAction(JoinMap.DirectoryEntrySelectedNumber.JoinNumber, s => PostStatusMessage(JToken.FromObject(new
+ {
+ directoryContactSelected = new
+ {
+ number = _eisc.GetString(JoinMap.DirectoryEntrySelectedNumber.JoinNumber),
+ }
+ })));
+
+ _eisc.SetStringSigAction(JoinMap.DirectorySelectedFolderName.JoinNumber, s => PostStatusMessage(JToken.FromObject(new
+ {
+ directorySelectedFolderName = _eisc.GetString(JoinMap.DirectorySelectedFolderName.JoinNumber)
+ })));
+
+ _eisc.SetSigTrueAction(JoinMap.CameraModeAuto.JoinNumber, PostCameraMode);
+ _eisc.SetSigTrueAction(JoinMap.CameraModeManual.JoinNumber, PostCameraMode);
+ _eisc.SetSigTrueAction(JoinMap.CameraModeOff.JoinNumber, PostCameraMode);
+
+ _eisc.SetBoolSigAction(JoinMap.CameraSelfView.JoinNumber, b => PostStatusMessage(JToken.FromObject(new
+ {
+ cameraSelfView = b
+ })));
+
+ _eisc.SetUShortSigAction(JoinMap.CameraNumberSelect.JoinNumber, u => PostSelectedCamera());
+
+
+ // Add press and holds using helper action
+ void addPhAction(string s, uint u) =>
+ AddAction(s, (id, content) => HandleCameraPressAndHold(content, b => _eisc.SetBool(u, b)));
+ addPhAction("/cameraUp", JoinMap.CameraTiltUp.JoinNumber);
+ addPhAction("/cameraDown", JoinMap.CameraTiltDown.JoinNumber);
+ addPhAction("/cameraLeft", JoinMap.CameraPanLeft.JoinNumber);
+ addPhAction("/cameraRight", JoinMap.CameraPanRight.JoinNumber);
+ addPhAction("/cameraZoomIn", JoinMap.CameraZoomIn.JoinNumber);
+ addPhAction("/cameraZoomOut", JoinMap.CameraZoomOut.JoinNumber);
+
+ // Add straight pulse calls using helper action
+ void addAction(string s, uint u) =>
+ AddAction(s, (id, content) => _eisc.PulseBool(u, 100));
+ addAction("/endCallById", JoinMap.EndCall.JoinNumber);
+ addAction("/endAllCalls", JoinMap.EndCall.JoinNumber);
+ addAction("/acceptById", JoinMap.IncomingAnswer.JoinNumber);
+ addAction("/rejectById", JoinMap.IncomingReject.JoinNumber);
+
+ var speeddialStart = JoinMap.SpeedDialStart.JoinNumber;
+ var speeddialEnd = JoinMap.SpeedDialStart.JoinNumber + JoinMap.SpeedDialStart.JoinSpan;
+
+ var speedDialIndex = 1;
+ for (uint i = speeddialStart; i < speeddialEnd; i++)
+ {
+ addAction(string.Format("/speedDial{0}", speedDialIndex), i);
+ speedDialIndex++;
+ }
+
+ addAction("/cameraModeAuto", JoinMap.CameraModeAuto.JoinNumber);
+ addAction("/cameraModeManual", JoinMap.CameraModeManual.JoinNumber);
+ addAction("/cameraModeOff", JoinMap.CameraModeOff.JoinNumber);
+ addAction("/cameraSelfView", JoinMap.CameraSelfView.JoinNumber);
+ addAction("/cameraLayout", JoinMap.CameraLayout.JoinNumber);
+
+ AddAction("/cameraSelect", (id, content) =>
+ {
+ var s = content.ToObject>();
+ SelectCamera(s.Value);
+ });
+
+ // camera presets
+ for (uint i = 0; i < 6; i++)
+ {
+ addAction("/cameraPreset" + (i + 1), JoinMap.CameraPresetStart.JoinNumber + i);
+ }
+
+ AddAction("/isReady", (id, content) => PostIsReady());
+ // Get status
+ AddAction("/fullStatus", (id, content) => PostFullStatus());
+ // Dial on string
+ AddAction("/dial", (id, content) =>
+ {
+ var s = content.ToObject>();
+
+ _eisc.SetString(JoinMap.CurrentDialString.JoinNumber, s.Value);
+ });
+ // Pulse DTMF
+ AddAction("/dtmf", (id, content) =>
+ {
+ var s = content.ToObject>();
+ var join = JoinMap.Joins[s.Value];
+ if (join != null)
+ {
+ if (join.JoinNumber > 0)
+ {
+ _eisc.PulseBool(join.JoinNumber, 100);
+ }
+ }
+ });
+
+ // Directory madness
+ AddAction("/directoryRoot",
+ (id, content) => _eisc.PulseBool(JoinMap.DirectoryRoot.JoinNumber));
+ AddAction("/directoryBack",
+ (id, content) => _eisc.PulseBool(JoinMap.DirectoryFolderBack.JoinNumber));
+ AddAction("/directoryById", (id, content) =>
+ {
+ var s = content.ToObject>();
+ // the id should contain the line number to forward to simpl
+ try
+ {
+ var u = ushort.Parse(s.Value);
+ _eisc.SetUshort(JoinMap.DirectorySelectRow.JoinNumber, u);
+ _eisc.PulseBool(JoinMap.DirectoryLineSelected.JoinNumber);
+ }
+ catch (Exception)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Warning,
+ "/directoryById request contains non-numeric ID incompatible with SIMPL bridge");
+ }
+ });
+ AddAction("/directorySelectContact", (id, content) =>
+ {
+ var s = content.ToObject>();
+ try
+ {
+ var u = ushort.Parse(s.Value);
+ _eisc.SetUshort(JoinMap.DirectorySelectRow.JoinNumber, u);
+ _eisc.PulseBool(JoinMap.DirectoryLineSelected.JoinNumber);
+ }
+ catch
+ {
+ Debug.Console(2, this, "Error parsing contact from {0} for path /directorySelectContact", s);
+ }
+ });
+ AddAction("/directoryDialContact",
+ (id, content) => _eisc.PulseBool(JoinMap.DirectoryDialSelectedLine.JoinNumber));
+ AddAction("/getDirectory", (id, content) =>
+ {
+ if (_eisc.GetUshort(JoinMap.DirectoryRowCount.JoinNumber) > 0)
+ {
+ PostDirectory();
+ }
+ else
+ {
+ _eisc.PulseBool(JoinMap.DirectoryRoot.JoinNumber);
+ }
+ });
+ }
+
+ private void HandleCameraPressAndHold(JToken content, Action cameraAction)
+ {
+ var state = content.ToObject>();
+
+ var timerHandler = PressAndHoldHandler.GetPressAndHoldHandler(state.Value);
+ if (timerHandler == null)
+ {
+ return;
+ }
+
+ timerHandler(state.Value, cameraAction);
+
+ cameraAction(state.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ private void PostFullStatus()
+ {
+ PostStatusMessage(JToken.FromObject(new
+ {
+ calls = GetCurrentCallList(),
+ cameraMode = GetCameraMode(),
+ cameraSelfView = _eisc.GetBool(JoinMap.CameraSelfView.JoinNumber),
+ cameraSupportsAutoMode = _eisc.GetBool(JoinMap.CameraSupportsAutoMode.JoinNumber),
+ cameraSupportsOffMode = _eisc.GetBool(JoinMap.CameraSupportsOffMode.JoinNumber),
+ currentCallString = _eisc.GetString(JoinMap.CurrentCallNumber.JoinNumber),
+ currentDialString = _eisc.GetString(JoinMap.CurrentDialString.JoinNumber),
+ directoryContactSelected = new
+ {
+ name = _eisc.GetString(JoinMap.DirectoryEntrySelectedName.JoinNumber),
+ number = _eisc.GetString(JoinMap.DirectoryEntrySelectedNumber.JoinNumber)
+ },
+ directorySelectedFolderName = _eisc.GetString(JoinMap.DirectorySelectedFolderName.JoinNumber),
+ isInCall = _eisc.GetString(JoinMap.HookState.JoinNumber) == "Connected",
+ hasDirectory = true,
+ hasDirectorySearch = false,
+ hasRecents = !_eisc.BooleanOutput[502].BoolValue,
+ hasCameras = true,
+ showCamerasWhenNotInCall = _eisc.BooleanOutput[503].BoolValue,
+ selectedCamera = GetSelectedCamera(),
+ }));
+ }
+
+ ///
+ ///
+ ///
+ private void PostDirectory()
+ {
+ var u = _eisc.GetUshort(JoinMap.DirectoryRowCount.JoinNumber);
+ var items = new List