From 51414e7d261f8394333cc403f8f41c59a08cd445 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 12 Mar 2025 13:23:40 -0500 Subject: [PATCH 01/26] chore(force-patch): increment patch version From daccf9eb775f9aaf6dfe8942340853ae634a33da Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 12 Mar 2025 13:37:13 -0500 Subject: [PATCH 02/26] chore(force-patch): increment patch version From 9ef869838792dd0449212d45b1b3534c91cdd470 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 12 Mar 2025 13:38:52 -0500 Subject: [PATCH 03/26] feat: increment minor version From a288fc5890f7d422dc688e50d7b912c6d8d6f26f Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 12 Mar 2025 13:41:21 -0500 Subject: [PATCH 04/26] fix: increment minor version From 4dcb3946c53e8edc3a279ead50833ff13a4e8247 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 12 Mar 2025 13:54:13 -0500 Subject: [PATCH 05/26] chore: remove unnecessary files --- .../CenIoCom/CenIoComController.cs | 85 -------- .../HdPsXxxAnalogAuxMixerController.cs | 185 ------------------ .../Chassis/HdPsXxxOutputAudioController.cs | 167 ---------------- 3 files changed, 437 deletions(-) delete mode 100644 essentials-framework/Essentials Core/PepperDashEssentialsBase/Crestron IO/CenIoCom/CenIoComController.cs delete mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/HdPsXxxAnalogAuxMixerController.cs delete mode 100644 essentials-framework/Essentials DM/Essentials_DM/Chassis/HdPsXxxOutputAudioController.cs 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 From fdb04286d6f11a3fa3780332b0f95590e86353b5 Mon Sep 17 00:00:00 2001 From: Jonathan Arndt Date: Sun, 23 Mar 2025 16:36:41 -0700 Subject: [PATCH 06/26] fix: correct event subscription logic in GenericCommunicationMonitor and poll inclusively. --- .../Monitoring/GenericCommunicationMonitor.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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) From d7499662deaf729153b47ffb537a1b46515b41bc Mon Sep 17 00:00:00 2001 From: Jonathan Arndt Date: Mon, 24 Mar 2025 11:01:12 -0700 Subject: [PATCH 07/26] fix: revise 4-series-caller.yml to include bypassPackageCheck bool. --- .github/workflows/EssentialsPlugins-builds-4-series-caller.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From f6fdc140598a16362961817ac3ca3fa05da03b2f Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 24 Mar 2025 22:28:27 -0500 Subject: [PATCH 08/26] feat: move MC into Essentials In order to solve some dependency issues that keep cropping up, MC should be moved back into the Essentials repo and loaded automatically on startup. This will allow for all plugins that use the MC Messengers library to use the same version without fear of overwriting a dll due to loading of plugin libraries. --- PepperDash.Essentials.4Series.sln | 27 + .../ContentTypes.cs | 31 + .../CoreDisplayBaseMessenger.cs | 61 + .../DisplayBaseMessenger.cs | 61 + .../IChannelMessenger.cs | 31 + .../DeviceTypeExtenstions/IColorMessenger.cs | 26 + .../DeviceTypeExtenstions/IDPadMessenger.cs | 31 + .../DeviceTypeExtenstions/IDvrMessenger.cs | 26 + .../IHasPowerMessenger.cs | 25 + .../INumericMessenger.cs | 36 + .../ISetTopBoxControlsMessenger.cs | 41 + .../ITransportMessenger.cs | 32 + .../Messengers/AudioCodecBaseMessenger.cs | 120 + .../Messengers/CameraBaseMessenger.cs | 209 ++ .../CoreTwoWayDisplayBaseMessenger.cs | 91 + .../Messengers/DeviceInfoMessenger.cs | 47 + .../Messengers/DevicePresetsModelMessenger.cs | 101 + .../Messengers/DeviceVolumeMessenger.cs | 174 ++ .../Messengers/GenericMessenger.cs | 31 + .../ICommunicationMonitorMessenger.cs | 79 + .../Messengers/IDspPresetsMessenger.cs | 50 + .../IEssentialsRoomCombinerMessenger.cs | 153 + .../IHasCurrentSourceInfoMessenger.cs | 57 + .../IHasPowerControlWithFeedbackMessenger.cs | 57 + .../IHasScheduleAwarenessMessenger.cs | 86 + .../Messengers/IHumiditySensor.cs | 43 + .../Messengers/ILevelControlsMessenger.cs | 95 + .../Messengers/IMatrixRoutingMessenger.cs | 168 ++ .../IProjectorScreenLiftControlMessenger.cs | 78 + .../Messengers/IRunRouteActionMessenger.cs | 89 + .../Messengers/ISelectableItemsMessenger.cs | 70 + .../IShutdownPromptTimerMessenger.cs | 93 + .../Messengers/ISwitchedOutputMessenger.cs | 67 + .../Messengers/ITechPasswordMessenger.cs | 95 + .../Messengers/ITemperatureSensorMessenger.cs | 61 + .../Messengers/LightingBaseMessenger.cs | 73 + .../Messengers/MessengerBase.cs | 303 ++ .../Messengers/PressAndHoldHandler.cs | 116 + .../Messengers/RoomEventScheduleMessenger.cs | 80 + .../Messengers/SIMPLAtcMessenger.cs | 163 + .../Messengers/SIMPLCameraMessenger.cs | 169 ++ .../Messengers/SIMPLDirectRouteMessenger.cs | 132 + .../Messengers/SIMPLRouteMessenger.cs | 77 + .../Messengers/SIMPLVtcMessenger.cs | 480 +++ .../Messengers/ShadeBaseMessenger.cs | 105 + .../SimplMessengerPropertiesConfig.cs | 11 + .../Messengers/SystemMonitorMessenger.cs | 116 + .../Messengers/TwoWayDisplayBaseMessenger.cs | 102 + .../Messengers/VideoCodecBaseMessenger.cs | 1002 ++++++ .../MobileControlMessage.cs | 23 + .../MobileControlSimpleContent.cs | 10 + ...Essentials.MobileControl.Messengers.csproj | 48 + .../MobileControlSIMPLRoomJoinMap.cs | 570 ++++ ...ControlSIMPLRunDirectRouteActionJoinMap.cs | 72 + .../SIMPLJoinMaps/SIMPLAtcJoinMap.cs | 247 ++ .../SIMPLJoinMaps/SIMPLVtcJoinMap.cs | 553 ++++ .../AuthorizationResponse.cs | 19 + .../Interfaces.cs | 16 + .../MobileControlAction.cs | 23 + .../MobileControlConfig.cs | 161 + .../MobileControlDeviceFactory.cs | 87 + .../MobileControlEssentialsConfig.cs | 54 + .../MobileControlFactory.cs | 41 + .../MobileControlSimplDeviceBridge.cs | 143 + .../MobileControlSystemController.cs | 2674 +++++++++++++++++ ...PepperDash.Essentials.MobileControl.csproj | 67 + .../RoomBridges/MobileControlBridgeBase.cs | 130 + .../MobileControlEssentialsRoomBridge.cs | 977 ++++++ .../MobileControlSIMPLRoomBridge.cs | 1128 +++++++ .../RoomBridges/SourceDeviceMapDictionary.cs | 96 + .../Services/MobileControlApiService.cs | 76 + .../Touchpanel/ITheme.cs | 17 + .../Touchpanel/ITswAppControl.cs | 25 + .../Touchpanel/ITswAppControlMessenger.cs | 59 + .../Touchpanel/ITswZoomControlMessenger.cs | 73 + .../MobileControlTouchpanelController.cs | 571 ++++ .../MobileControlTouchpanelProperties.cs | 20 + .../Touchpanel/ThemeMessenger.cs | 42 + .../TransmitMessage.cs | 150 + .../UserCodeChangedContent.cs | 13 + .../Volumes.cs | 76 + .../WebApiHandlers/ActionPathsHandler.cs | 52 + .../MobileAuthRequestHandler.cs | 59 + .../WebApiHandlers/MobileInfoHandler.cs | 159 + .../WebApiHandlers/UiClientHandler.cs | 164 + .../MobileControlWebsocketServer.cs | 1404 +++++++++ .../WebSocketServerSecretProvider.cs | 37 + src/PepperDash.Essentials/ControlSystem.cs | 23 +- .../PepperDash.Essentials.csproj | 2 + 89 files changed, 15625 insertions(+), 7 deletions(-) create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/CoreDisplayBaseMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/DisplayBaseMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IChannelMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IColorMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDPadMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDvrMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IHasPowerMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/INumericMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ISetTopBoxControlsMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ITransportMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CoreTwoWayDisplayBaseMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SimplMessengerPropertiesConfig.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/MobileControlSimpleContent.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/MobileControlSIMPLRoomJoinMap.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/MobileControlSIMPLRunDirectRouteActionJoinMap.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLAtcJoinMap.cs create mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLVtcJoinMap.cs create mode 100644 src/PepperDash.Essentials.MobileControl/AuthorizationResponse.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Interfaces.cs create mode 100644 src/PepperDash.Essentials.MobileControl/MobileControlAction.cs create mode 100644 src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs create mode 100644 src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs create mode 100644 src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs create mode 100644 src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs create mode 100644 src/PepperDash.Essentials.MobileControl/MobileControlSimplDeviceBridge.cs create mode 100644 src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs create mode 100644 src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj create mode 100644 src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs create mode 100644 src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs create mode 100644 src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs create mode 100644 src/PepperDash.Essentials.MobileControl/RoomBridges/SourceDeviceMapDictionary.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs create mode 100644 src/PepperDash.Essentials.MobileControl/TransmitMessage.cs create mode 100644 src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs create mode 100644 src/PepperDash.Essentials.MobileControl/Volumes.cs create mode 100644 src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs create mode 100644 src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs create mode 100644 src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs create mode 100644 src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs create mode 100644 src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs create mode 100644 src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs diff --git a/PepperDash.Essentials.4Series.sln b/PepperDash.Essentials.4Series.sln index e2db852a..a1e57f5e 100644 --- a/PepperDash.Essentials.4Series.sln +++ b/PepperDash.Essentials.4Series.sln @@ -9,6 +9,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials", "sr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Core", "src\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj", "{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}" 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}" +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}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Essentials", "Essentials", "{AD98B742-8D85-481C-A69D-D8D8ABED39EA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU @@ -34,10 +42,29 @@ 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 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} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3} EndGlobalSection diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs b/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs new file mode 100644 index 00000000..476747b0 --- /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/DeviceTypeExtenstions/CoreDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/CoreDisplayBaseMessenger.cs new file mode 100644 index 00000000..bf782710 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/CoreDisplayBaseMessenger.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.AppServer; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using System; +using System.Linq; +using DisplayBase = PepperDash.Essentials.Core.DisplayBase; + +namespace PepperDash.Essentials.Room.MobileControl +{ + public class CoreDisplayBaseMessenger: MessengerBase + { + private readonly DisplayBase display; + + public CoreDisplayBaseMessenger(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) + { + Debug.Console(1, "No input named {0} found for device {1}", 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/DeviceTypeExtenstions/DisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/DisplayBaseMessenger.cs new file mode 100644 index 00000000..659c62e8 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/DisplayBaseMessenger.cs @@ -0,0 +1,61 @@ +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.AppServer; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using System; +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) + { + Debug.Console(1, "No input named {0} found for device {1}", 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/DeviceTypeExtenstions/IChannelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IChannelMessenger.cs new file mode 100644 index 00000000..33d5ecd7 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IChannelMessenger.cs @@ -0,0 +1,31 @@ +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +#if SERIES4 +#endif +namespace PepperDash.Essentials.Room.MobileControl +{ + public class IChannelMessenger:MessengerBase + { + private readonly IChannel channelDevice; + + public IChannelMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + { + channelDevice = device as IChannel; + } + + 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/DeviceTypeExtenstions/IColorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IColorMessenger.cs new file mode 100644 index 00000000..d23fbf2b --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IColorMessenger.cs @@ -0,0 +1,26 @@ +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; + +namespace PepperDash.Essentials.Room.MobileControl +{ + public class IColorMessenger:MessengerBase + { + private readonly IColor colorDevice; + public IColorMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + { + 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/DeviceTypeExtenstions/IDPadMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDPadMessenger.cs new file mode 100644 index 00000000..6f72856c --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDPadMessenger.cs @@ -0,0 +1,31 @@ +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +#if SERIES4 +#endif +namespace PepperDash.Essentials.Room.MobileControl +{ + public class IDPadMessenger:MessengerBase + { + private readonly IDPad dpadDevice; + public IDPadMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + { + dpadDevice = device as IDPad; + } + + + 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/DeviceTypeExtenstions/IDvrMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDvrMessenger.cs new file mode 100644 index 00000000..4692aaf0 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDvrMessenger.cs @@ -0,0 +1,26 @@ +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +#if SERIES4 +#endif +namespace PepperDash.Essentials.Room.MobileControl +{ + public class IDvrMessenger: MessengerBase + { + private readonly IDvr dvrDevice; + public IDvrMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + { + dvrDevice = device as IDvr; + } + + 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/DeviceTypeExtenstions/IHasPowerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IHasPowerMessenger.cs new file mode 100644 index 00000000..33ce7ea1 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IHasPowerMessenger.cs @@ -0,0 +1,25 @@ +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; + +namespace PepperDash.Essentials.Room.MobileControl +{ + public class IHasPowerMessenger:MessengerBase + { + private readonly IHasPowerControl powerDevice; + public IHasPowerMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + { + powerDevice = device as IHasPowerControl; + } + + 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/DeviceTypeExtenstions/INumericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/INumericMessenger.cs new file mode 100644 index 00000000..dc3290c9 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/INumericMessenger.cs @@ -0,0 +1,36 @@ +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +#if SERIES4 +#endif +namespace PepperDash.Essentials.Room.MobileControl +{ + public class INumericKeypadMessenger:MessengerBase + { + private readonly INumericKeypad keypadDevice; + public INumericKeypadMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + { + keypadDevice = device as INumericKeypad; + } + + 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/DeviceTypeExtenstions/ISetTopBoxControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ISetTopBoxControlsMessenger.cs new file mode 100644 index 00000000..9b88b035 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ISetTopBoxControlsMessenger.cs @@ -0,0 +1,41 @@ +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +#if SERIES4 +#endif +namespace PepperDash.Essentials.Room.MobileControl +{ + public class ISetTopBoxControlsMessenger:MessengerBase + { + private readonly ISetTopBoxControls stbDevice; + public ISetTopBoxControlsMessenger(string key, string messagePath, IKeyName device) : base(key, messagePath, device) + { + stbDevice = device as ISetTopBoxControls; + } + + 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/DeviceTypeExtenstions/ITransportMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ITransportMessenger.cs new file mode 100644 index 00000000..bc2f770e --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ITransportMessenger.cs @@ -0,0 +1,32 @@ +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +#if SERIES4 +#endif +namespace PepperDash.Essentials.Room.MobileControl +{ + public class ITransportMessenger:MessengerBase + { + private readonly ITransport transportDevice; + public ITransportMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + { + transportDevice = device as ITransport; + } + + 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..0472241d --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs @@ -0,0 +1,120 @@ +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +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; + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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..1a8f0706 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs @@ -0,0 +1,209 @@ +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +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 + }) + ); + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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/CoreTwoWayDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CoreTwoWayDisplayBaseMessenger.cs new file mode 100644 index 00000000..8bce8a4e --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CoreTwoWayDisplayBaseMessenger.cs @@ -0,0 +1,91 @@ +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + public class CoreTwoWayDisplayBaseMessenger : MessengerBase + { + private readonly TwoWayDisplayBase _display; + + public CoreTwoWayDisplayBaseMessenger(string key, string messagePath, Device display) + : base(key, messagePath, display) + { + _display = display as TwoWayDisplayBase; + } + + #region Overrides of MessengerBase + + public void SendFullStatus() + { + var messageObj = new TwoWayDisplayBaseStateMessage + { + //PowerState = _display.PowerIsOnFeedback.BoolValue, + CurrentInput = _display.CurrentInputFeedback.StringValue + }; + + PostStatusMessage(messageObj); + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + base.RegisterActions(); + if (_display == null) + { + Debug.Console(0, this, $"Unable to register TwoWayDisplayBase messenger {Key}"); + return; + } + + AddAction("/fullStatus", (id, content) => SendFullStatus()); + + _display.PowerIsOnFeedback.OutputChange += PowerIsOnFeedbackOnOutputChange; + _display.CurrentInputFeedback.OutputChange += CurrentInputFeedbackOnOutputChange; + _display.IsCoolingDownFeedback.OutputChange += IsCoolingFeedbackOnOutputChange; + _display.IsWarmingUpFeedback.OutputChange += IsWarmingFeedbackOnOutputChange; + } + + private void CurrentInputFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + { + PostStatusMessage(JToken.FromObject(new + { + currentInput = feedbackEventArgs.StringValue + })); + } + + + private void PowerIsOnFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + { + PostStatusMessage(JToken.FromObject(new + { + powerState = feedbackEventArgs.BoolValue + }) + ); + } + + private void IsWarmingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + { + PostStatusMessage(JToken.FromObject(new + { + isWarming = feedbackEventArgs.BoolValue + }) + ); + + } + + private void IsCoolingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + { + PostStatusMessage(JToken.FromObject(new + { + isCooling = feedbackEventArgs.BoolValue + }) + ); + } + + #endregion + } +} 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..8a17ce01 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs @@ -0,0 +1,47 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core.DeviceInfo; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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..36c6fd62 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs @@ -0,0 +1,101 @@ +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 + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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..aeeb3af0 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs @@ -0,0 +1,174 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +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 + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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..2a52db13 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs @@ -0,0 +1,31 @@ +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using System; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + public class GenericMessenger : MessengerBase + { + public GenericMessenger(string key, EssentialsDevice device, string messagePath) : base(key, messagePath, device) + { + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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..7e4d03f6 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs @@ -0,0 +1,79 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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..31529566 --- /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 IDspPresets _device; + + public IDspPresetsMessenger(string key, string messagePath, IDspPresets device) + : base(key, messagePath, device as Device) + { + _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..9601bad6 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs @@ -0,0 +1,153 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +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 Device) + { + _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) + { + Debug.Console(0, this, "Error sending full status: {0}", e); + } + } + + 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..65bc67bd --- /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; +using PepperDash.Essentials.Core.Routing; + +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..bf838e92 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs @@ -0,0 +1,57 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core; +using PepperDash.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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 Device) + { + _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..056f2f22 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs @@ -0,0 +1,86 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +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 Device) + { + ScheduleSource = scheduleSource ?? throw new ArgumentNullException("scheduleSource"); + ScheduleSource.CodecSchedule.MeetingsListHasChanged += new EventHandler(CodecSchedule_MeetingsListHasChanged); + ScheduleSource.CodecSchedule.MeetingEventChange += new EventHandler(CodecSchedule_MeetingEventChange); + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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..0e500250 --- /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 Device) + { + 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..6410d314 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs @@ -0,0 +1,95 @@ +using Independentsoft.Exchange; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +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 Device) + { + 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..8bacab8f --- /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 System.Collections.Generic; +using System.Linq; +using Serilog.Events; +using System; + +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 Device) + { + 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..9d4a3804 --- /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 Device) + { + 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..88d0a5bf --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs @@ -0,0 +1,89 @@ +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +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 Device) + { + RoutingDevice = routingDevice ?? throw new ArgumentNullException("routingDevice"); + + + if (RoutingDevice is IRoutingSink routingSink) + { + routingSink.CurrentSourceChange += RoutingSink_CurrentSourceChange; + } + } + + private void RoutingSink_CurrentSourceChange(SourceListItem info, ChangeType type) + { + SendRoutingFullMessageObject(); + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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 + Debug.Console(1, this, "sourceListKey found in 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..88441526 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs @@ -0,0 +1,70 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json.Converters; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + public class ISelectableItemsMessenger : MessengerBase + { + private static readonly JsonSerializer serializer = new JsonSerializer { Converters = { new StringEnumConverter() } }; + private ISelectableItems itemDevice; + + private readonly string _propName; + public ISelectableItemsMessenger(string key, string messagePath, ISelectableItems device, string propName) : base(key, messagePath, device as Device) + { + 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..a2e6a6f8 --- /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 Device) + { + _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..6d3bceb6 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using PepperDash.Essentials.Core.CrestronIO; +using PepperDash.Essentials.Core.Shades; +using Newtonsoft.Json; +using PepperDash.Core; + +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 Device) + { + this.device = device; + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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..46e2a5a7 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Independentsoft.Json.Parser; +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 Device) + { + _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; } + } + + 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..8d1d5771 --- /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 Device) + { + 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..4058c3a7 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs @@ -0,0 +1,73 @@ +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +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 Device) + { + 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); + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus()); + + AddAction("/selectScene", (id, content) => + { + var s = content.ToObject(); + Device.SelectScene(s); + }); + } + + + private void SendFullStatus() + { + Debug.Console(2, "LightingBaseMessenger GetFullStatus"); + + 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..e73a4f0a --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -0,0 +1,303 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +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 + /// +#if SERIES4 + public abstract class MessengerBase : EssentialsDevice, IMobileControlMessenger +#else + public abstract class MessengerBase: EssentialsDevice +#endif + { + protected IKeyName _device; + + private readonly List _deviceInterfaces; + + private readonly Dictionary> _actions = new Dictionary>(); + + public string DeviceKey => _device?.Key ?? ""; + + /// + /// + /// +#if SERIES4 + public IMobileControl AppServerController { get; private set; } +#else + public MobileControlSystemController AppServerController { get; private set; } +#endif + + 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 + /// + /// +#if SERIES4 + public void RegisterWithAppServer(IMobileControl appServerController) +#else + public void RegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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 + /// + /// +#if SERIES4 + protected virtual void RegisterActions() +#else + protected virtual void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + + } + + /// + /// 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); + } + } + +#if SERIES4 + 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); + } + } +#endif + 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..2bf213ac --- /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; +using System.Runtime.CompilerServices; + +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..a32eb5a6 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +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 Device) + { + _room = room; + } + + #region Overrides of MessengerBase + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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) + { + Debug.Console(0, this, "Exception saving event: {0}\r\n{1}", ex.Message, ex.StackTrace); + } + } + + 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..3f2ab694 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs @@ -0,0 +1,163 @@ +using Crestron.SimplSharpPro.DeviceSupport; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +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" + }) + ); + } + + /// + /// + /// + /// +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + //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..42111467 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs @@ -0,0 +1,169 @@ +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()); + } + + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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)); + } + +#if SERIES4 + public void CustomUnregsiterWithAppServer(IMobileControl appServerController) +#else + public void CustomUnregsiterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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..46899cd1 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs @@ -0,0 +1,132 @@ +using Crestron.SimplSharpPro.DeviceSupport; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +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 + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController controller) +#endif + { + 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..ccdbb279 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs @@ -0,0 +1,77 @@ +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); + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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); + }); + } + +#if SERIES4 + public void CustomUnregsiterWithAppServer(IMobileControl appServerController) +#else + public void CustomUnregsiterWithAppServer(MobileControlSystemController appServerController) +#endif + { + 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..a421e068 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs @@ -0,0 +1,480 @@ +using Crestron.SimplSharpPro.DeviceSupport; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +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-" }; + } + + /// + /// + /// + /// +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + _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(); + for (uint i = 0; i < u; i++) + { + var name = _eisc.GetString(JoinMap.DirectoryEntriesStart.JoinNumber + i); + var id = (i + 1).ToString(); + // is folder or contact? + if (name.StartsWith("[+]")) + { + items.Add(new + { + folderId = id, + name + }); + } + else + { + items.Add(new + { + contactId = id, + name + }); + } + } + + var directoryMessage = new + { + currentDirectory = new + { + isRootDirectory = _eisc.GetBool(JoinMap.DirectoryIsRoot.JoinNumber), + directoryResults = items + } + }; + PostStatusMessage(JToken.FromObject(directoryMessage)); + } + + /// + /// + /// + private void PostCameraMode() + { + PostStatusMessage(JToken.FromObject(new + { + cameraMode = GetCameraMode() + })); + } + + /// + /// + /// + 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; + } + + private void PostSelectedCamera() + { + PostStatusMessage(JToken.FromObject(new + { + selectedCamera = GetSelectedCamera() + })); + } + + /// + /// + /// + private string GetSelectedCamera() + { + var num = _eisc.GetUshort(JoinMap.CameraNumberSelect.JoinNumber); + string m; + if (num == 100) + { + m = "cameraFar"; + } + else + { + m = "camera" + num; + } + return m; + } + + /// + /// + /// + private void PostIsReady() + { + PostStatusMessage(JToken.FromObject(new + { + isReady = true + })); + } + + /// + /// + /// + private void PostCallsList() + { + PostStatusMessage(JToken.FromObject(new + { + calls = GetCurrentCallList(), + })); + } + + /// + /// + /// + /// + private void SelectCamera(string s) + { + var cam = s.Substring(6); + _eisc.SetUshort(JoinMap.CameraNumberSelect.JoinNumber, + (ushort)(cam.ToLower() == "far" ? 100 : ushort.Parse(cam))); + } + + /// + /// Turns the + /// + /// + private List GetCurrentCallList() + { + var list = new List(); + if (_currentCallItem.Status != eCodecCallStatus.Disconnected) + { + list.Add(_currentCallItem); + } + if (_eisc.GetBool(JoinMap.IncomingCall.JoinNumber)) + { + list.Add(_incomingCallItem); + } + return list; + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs new file mode 100644 index 00000000..e004153b --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs @@ -0,0 +1,105 @@ +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.Shades; +using System; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + public class IShadesOpenCloseStopMessenger : MessengerBase + { + private readonly IShadesOpenCloseStop device; + + public IShadesOpenCloseStopMessenger(string key, IShadesOpenCloseStop shades, string messagePath) + : base(key, messagePath, shades as Device) + { + device = shades; + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus()); + + AddAction("/shadeUp", (id, content) => + { + + device.Open(); + + }); + + AddAction("/shadeDown", (id, content) => + { + + device.Close(); + + }); + + var stopDevice = device; + if (stopDevice != null) + { + AddAction("/stopOrPreset", (id, content) => + { + stopDevice.Stop(); + }); + } + + if (device is IShadesOpenClosedFeedback feedbackDevice) + { + feedbackDevice.ShadeIsOpenFeedback.OutputChange += new EventHandler(ShadeIsOpenFeedback_OutputChange); + feedbackDevice.ShadeIsClosedFeedback.OutputChange += new EventHandler(ShadeIsClosedFeedback_OutputChange); + } + } + + private void ShadeIsOpenFeedback_OutputChange(object sender, Core.FeedbackEventArgs e) + { + var state = new ShadeBaseStateMessage + { + IsOpen = e.BoolValue + }; + + PostStatusMessage(state); + } + + private void ShadeIsClosedFeedback_OutputChange(object sender, Core.FeedbackEventArgs e) + { + var state = new ShadeBaseStateMessage + { + IsClosed = e.BoolValue + }; + + PostStatusMessage(state); + } + + + private void SendFullStatus() + { + var state = new ShadeBaseStateMessage(); + + if (device is IShadesOpenClosedFeedback feedbackDevice) + { + state.IsOpen = feedbackDevice.ShadeIsOpenFeedback.BoolValue; + state.IsClosed = feedbackDevice.ShadeIsClosedFeedback.BoolValue; + } + + PostStatusMessage(state); + } + } + + public class ShadeBaseStateMessage : DeviceStateMessageBase + { + [JsonProperty("middleButtonLabel", NullValueHandling = NullValueHandling.Ignore)] + public string MiddleButtonLabel { get; set; } + + [JsonProperty("isOpen", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsOpen { get; set; } + + [JsonProperty("isClosed", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsClosed { get; set; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SimplMessengerPropertiesConfig.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SimplMessengerPropertiesConfig.cs new file mode 100644 index 00000000..ce4db62d --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SimplMessengerPropertiesConfig.cs @@ -0,0 +1,11 @@ +using PepperDash.Essentials.Core.Bridges; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Properties to configure a SIMPL Messenger + /// + public class SimplMessengerPropertiesConfig : EiscApiPropertiesConfig.ApiDevicePropertiesConfig + { + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs new file mode 100644 index 00000000..92ffd638 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs @@ -0,0 +1,116 @@ +using Crestron.SimplSharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.Monitoring; +using System; +using System.Threading.Tasks; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + public class SystemMonitorMessenger : MessengerBase + { + private readonly SystemMonitorController systemMonitor; + + public SystemMonitorMessenger(string key, SystemMonitorController sysMon, string messagePath) + : base(key, messagePath, sysMon) + { + systemMonitor = sysMon ?? throw new ArgumentNullException("sysMon"); + + systemMonitor.SystemMonitorPropertiesChanged += SysMon_SystemMonitorPropertiesChanged; + + foreach (var p in systemMonitor.ProgramStatusFeedbackCollection) + { + p.Value.ProgramInfoChanged += ProgramInfoChanged; + } + + CrestronConsole.AddNewConsoleCommand(s => SendFullStatusMessage(), "SendFullSysMonStatus", + "Sends the full System Monitor Status", ConsoleAccessLevelEnum.AccessOperator); + } + + /// + /// Posts the program information message + /// + /// + /// + private void ProgramInfoChanged(object sender, ProgramInfoEventArgs e) + { + if (e.ProgramInfo != null) + { + //Debug.Console(1, "Posting Status Message: {0}", e.ProgramInfo.ToString()); + PostStatusMessage(JToken.FromObject(e.ProgramInfo) + ); + } + } + + /// + /// Posts the system monitor properties + /// + /// + /// + private void SysMon_SystemMonitorPropertiesChanged(object sender, EventArgs e) + { + SendSystemMonitorStatusMessage(); + } + + private void SendFullStatusMessage() + { + SendSystemMonitorStatusMessage(); + + foreach (var p in systemMonitor.ProgramStatusFeedbackCollection) + { + PostStatusMessage(JToken.FromObject(p.Value.ProgramInfo) + ); + } + } + + private void SendSystemMonitorStatusMessage() + { + Debug.Console(1, "Posting System Monitor Status Message."); + + // This takes a while, launch a new thread + Task.Run(() => PostStatusMessage(JToken.FromObject(new SystemMonitorStateMessage + { + + TimeZone = systemMonitor.TimeZoneFeedback.IntValue, + TimeZoneName = systemMonitor.TimeZoneTextFeedback.StringValue, + IoControllerVersion = systemMonitor.IoControllerVersionFeedback.StringValue, + SnmpVersion = systemMonitor.SnmpVersionFeedback.StringValue, + BacnetVersion = systemMonitor.BaCnetAppVersionFeedback.StringValue, + ControllerVersion = systemMonitor.ControllerVersionFeedback.StringValue + }) + )); + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + AddAction("/fullStatus", (id, content) => SendFullStatusMessage()); + } + } + + public class SystemMonitorStateMessage + { + [JsonProperty("timeZone", NullValueHandling = NullValueHandling.Ignore)] + public int TimeZone { get; set; } + + [JsonProperty("timeZone", NullValueHandling = NullValueHandling.Ignore)] + public string TimeZoneName { get; set; } + + [JsonProperty("timeZone", NullValueHandling = NullValueHandling.Ignore)] + public string IoControllerVersion { get; set; } + + [JsonProperty("timeZone", NullValueHandling = NullValueHandling.Ignore)] + public string SnmpVersion { get; set; } + + [JsonProperty("timeZone", NullValueHandling = NullValueHandling.Ignore)] + public string BacnetVersion { get; set; } + + [JsonProperty("timeZone", NullValueHandling = NullValueHandling.Ignore)] + public string ControllerVersion { get; set; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs new file mode 100644 index 00000000..baf970db --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs @@ -0,0 +1,102 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using TwoWayDisplayBase = PepperDash.Essentials.Devices.Common.Displays.TwoWayDisplayBase; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + public class TwoWayDisplayBaseMessenger : MessengerBase + { + private readonly TwoWayDisplayBase _display; + + public TwoWayDisplayBaseMessenger(string key, string messagePath) : base(key, messagePath) + { + } + + public TwoWayDisplayBaseMessenger(string key, string messagePath, TwoWayDisplayBase display) + : this(key, messagePath) + { + _display = display; + } + + #region Overrides of MessengerBase + + public void SendFullStatus() + { + var messageObj = new TwoWayDisplayBaseStateMessage + { + //PowerState = _display.PowerIsOnFeedback.BoolValue, + CurrentInput = _display.CurrentInputFeedback.StringValue + }; + + PostStatusMessage(messageObj); + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + base.RegisterActions(); + + AddAction("/fullStatus", (id, content) => SendFullStatus()); + + //_display.PowerIsOnFeedback.OutputChange += PowerIsOnFeedbackOnOutputChange; + _display.CurrentInputFeedback.OutputChange += CurrentInputFeedbackOnOutputChange; + _display.IsCoolingDownFeedback.OutputChange += IsCoolingFeedbackOnOutputChange; + _display.IsWarmingUpFeedback.OutputChange += IsWarmingFeedbackOnOutputChange; + } + + private void CurrentInputFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + { + PostStatusMessage(JToken.FromObject(new + { + currentInput = feedbackEventArgs.StringValue + }) + ); + } + + + //private void PowerIsOnFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + //{ + // PostStatusMessage(JToken.FromObject(new + // { + // powerState = feedbackEventArgs.BoolValue + // }) + // ); + //} + + private void IsWarmingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + { + PostStatusMessage(JToken.FromObject(new + { + isWarming = feedbackEventArgs.BoolValue + }) + ); + } + + private void IsCoolingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) + { + PostStatusMessage(JToken.FromObject(new + { + isCooling = feedbackEventArgs.BoolValue + }) + ); + + + } + + #endregion + } + + public class TwoWayDisplayBaseStateMessage : DeviceStateMessageBase + { + //[JsonProperty("powerState", NullValueHandling = NullValueHandling.Ignore)] + //public bool? PowerState { get; set; } + + [JsonProperty("currentInput", NullValueHandling = NullValueHandling.Ignore)] + public string CurrentInput { get; set; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs new file mode 100644 index 00000000..fecbc688 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs @@ -0,0 +1,1002 @@ +using Crestron.SimplSharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Devices.Common.VideoCodec; +using PepperDash.Essentials.Devices.Common.VideoCodec.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using static PepperDash.Essentials.AppServer.Messengers.VideoCodecBaseStateMessage.CameraStatus; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + /// + /// Provides a messaging bridge for a VideoCodecBase device + /// + public class VideoCodecBaseMessenger : MessengerBase + { + /// + /// + /// + protected VideoCodecBase Codec { get; private set; } + + + + /// + /// + /// + /// + /// + /// + public VideoCodecBaseMessenger(string key, VideoCodecBase codec, string messagePath) + : base(key, messagePath, codec) + { + Codec = codec ?? throw new ArgumentNullException("codec"); + codec.CallStatusChange += Codec_CallStatusChange; + codec.IsReadyChange += Codec_IsReadyChange; + + if (codec is IHasDirectory dirCodec) + { + dirCodec.DirectoryResultReturned += DirCodec_DirectoryResultReturned; + } + + if (codec is IHasCallHistory recCodec) + { + recCodec.CallHistory.RecentCallsListHasChanged += CallHistory_RecentCallsListHasChanged; + } + + if (codec is IPasswordPrompt pwPromptCodec) + { + pwPromptCodec.PasswordRequired += OnPasswordRequired; + } + } + + private void OnPasswordRequired(object sender, PasswordPromptEventArgs args) + { + var eventMsg = new PasswordPromptEventMessage + { + Message = args.Message, + LastAttemptWasIncorrect = args.LastAttemptWasIncorrect, + LoginAttemptFailed = args.LoginAttemptFailed, + LoginAttemptCancelled = args.LoginAttemptCancelled, + EventType = "passwordPrompt" + }; + + PostEventMessage(eventMsg); + } + + /// + /// + /// + /// + /// + private void CallHistory_RecentCallsListHasChanged(object sender, EventArgs e) + { + var state = new VideoCodecBaseStateMessage(); + + if (!(sender is CodecCallHistory codecCallHistory)) return; + var recents = codecCallHistory.RecentCalls; + + if (recents != null) + { + state.RecentCalls = recents; + + PostStatusMessage(state); + } + } + + /// + /// + /// + /// + /// + protected virtual void DirCodec_DirectoryResultReturned(object sender, DirectoryEventArgs e) + { + if (Codec is IHasDirectory) + SendDirectory(e.Directory); + } + + /// + /// Posts the current directory + /// + protected void SendDirectory(CodecDirectory directory) + { + var state = new VideoCodecBaseStateMessage(); + + + if (Codec is IHasDirectory dirCodec) + { + Debug.Console(2, this, "Sending Directory. Directory Item Count: {0}", directory.CurrentDirectoryResults.Count); + + //state.CurrentDirectory = PrefixDirectoryFolderItems(directory); + state.CurrentDirectory = directory; + CrestronInvoke.BeginInvoke((o) => PostStatusMessage(state)); + + /* var directoryMessage = new + { + currentDirectory = new + { + directoryResults = prefixedDirectoryResults, + isRootDirectory = isRoot + } + }; + + //Spool up a thread in case this is a large quantity of data + CrestronInvoke.BeginInvoke((o) => PostStatusMessage(directoryMessage)); */ + } + } + + /// + /// + /// + /// + /// + private void Codec_IsReadyChange(object sender, EventArgs e) + { + var state = new VideoCodecBaseStateMessage + { + IsReady = true + }; + + PostStatusMessage(state); + + SendFullStatus(); + } + + /// + /// Called from base's RegisterWithAppServer method + /// + /// +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + try + { + base.RegisterActions(); + + AddAction("/isReady", (id, content) => SendIsReady()); + + AddAction("/fullStatus", (id, content) => SendFullStatus()); + + AddAction("/dial", (id, content) => + { + var value = content.ToObject>(); + + Codec.Dial(value.Value); + }); + + AddAction("/dialMeeting", (id, content) => Codec.Dial(content.ToObject())); + + AddAction("/endCallById", (id, content) => + { + var s = content.ToObject>(); + var call = GetCallWithId(s.Value); + if (call != null) + Codec.EndCall(call); + }); + + AddAction("/endAllCalls", (id, content) => Codec.EndAllCalls()); + + AddAction("/dtmf", (id, content) => + { + var s = content.ToObject>(); + Codec.SendDtmf(s.Value); + }); + + AddAction("/rejectById", (id, content) => + { + var s = content.ToObject>(); + + var call = GetCallWithId(s.Value); + if (call != null) + Codec.RejectCall(call); + }); + + AddAction("/acceptById", (id, content) => + { + var s = content.ToObject>(); + + var call = GetCallWithId(s.Value); + if (call != null) + Codec.AcceptCall(call); + }); + + Codec.SharingContentIsOnFeedback.OutputChange += SharingContentIsOnFeedback_OutputChange; + Codec.SharingSourceFeedback.OutputChange += SharingSourceFeedback_OutputChange; + + // Directory actions + if (Codec is IHasDirectory dirCodec) + { + AddAction("/getDirectory", (id, content) => GetDirectoryRoot()); + + AddAction("/directoryById", (id, content) => + { + var msg = content.ToObject>(); + GetDirectory(msg.Value); + }); + + AddAction("/directorySearch", (id, content) => + { + var msg = content.ToObject>(); + + GetDirectory(msg.Value); + }); + + AddAction("/directoryBack", (id, content) => GetPreviousDirectory()); + + dirCodec.PhonebookSyncState.InitialSyncCompleted += PhonebookSyncState_InitialSyncCompleted; + } + + // History actions + if (Codec is IHasCallHistory recCodec) + { + AddAction("/getCallHistory", (id, content) => PostCallHistory()); + } + if (Codec is IHasCodecCameras cameraCodec) + { + Debug.Console(2, this, "Adding IHasCodecCameras Actions"); + + cameraCodec.CameraSelected += CameraCodec_CameraSelected; + + AddAction("/cameraSelect", (id, content) => + { + var msg = content.ToObject>(); + + cameraCodec.SelectCamera(msg.Value); + }); + + + MapCameraActions(); + + if (Codec is IHasCodecRoomPresets presetsCodec) + { + Debug.Console(2, this, "Adding IHasCodecRoomPresets Actions"); + + presetsCodec.CodecRoomPresetsListHasChanged += PresetsCodec_CameraPresetsListHasChanged; + + AddAction("/cameraPreset", (id, content) => + { + var msg = content.ToObject>(); + + presetsCodec.CodecRoomPresetSelect(msg.Value); + }); + + AddAction("/cameraPresetStore", (id, content) => + { + var msg = content.ToObject(); + + presetsCodec.CodecRoomPresetStore(msg.ID, msg.Description); + }); + } + + if (Codec is IHasCameraAutoMode speakerTrackCodec) + { + Debug.Console(2, this, "Adding IHasCameraAutoMode Actions"); + + speakerTrackCodec.CameraAutoModeIsOnFeedback.OutputChange += CameraAutoModeIsOnFeedback_OutputChange; + + AddAction("/cameraModeAuto", (id, content) => speakerTrackCodec.CameraAutoModeOn()); + + AddAction("/cameraModeManual", (id, content) => speakerTrackCodec.CameraAutoModeOff()); + } + + if (Codec is IHasCameraOff cameraOffCodec) + { + Debug.Console(2, this, "Adding IHasCameraOff Actions"); + + cameraOffCodec.CameraIsOffFeedback.OutputChange += (CameraIsOffFeedback_OutputChange); + + AddAction("/cameraModeOff", (id, content) => cameraOffCodec.CameraOff()); + } + } + + + + if (Codec is IHasCodecSelfView selfViewCodec) + { + Debug.Console(2, this, "Adding IHasCodecSelfView Actions"); + + AddAction("/cameraSelfView", (id, content) => selfViewCodec.SelfViewModeToggle()); + + selfViewCodec.SelfviewIsOnFeedback.OutputChange += new EventHandler(SelfviewIsOnFeedback_OutputChange); + } + + + if (Codec is IHasCodecLayouts layoutsCodec) + { + Debug.Console(2, this, "Adding IHasCodecLayouts Actions"); + + AddAction("/cameraRemoteView", (id, content) => layoutsCodec.LocalLayoutToggle()); + + AddAction("/cameraLayout", (id, content) => layoutsCodec.LocalLayoutToggle()); + } + + if (Codec is IPasswordPrompt pwCodec) + { + Debug.Console(2, this, "Adding IPasswordPrompt Actions"); + + AddAction("/password", (id, content) => + { + var msg = content.ToObject>(); + + pwCodec.SubmitPassword(msg.Value); + }); + } + + + if (Codec is IHasFarEndContentStatus farEndContentStatus) + { + farEndContentStatus.ReceivingContent.OutputChange += + (sender, args) => PostReceivingContent(args.BoolValue); + } + + Debug.Console(2, this, "Adding Privacy & Standby Actions"); + + AddAction("/privacyModeOn", (id, content) => Codec.PrivacyModeOn()); + AddAction("/privacyModeOff", (id, content) => Codec.PrivacyModeOff()); + AddAction("/privacyModeToggle", (id, content) => Codec.PrivacyModeToggle()); + AddAction("/sharingStart", (id, content) => Codec.StartSharing()); + AddAction("/sharingStop", (id, content) => Codec.StopSharing()); + AddAction("/standbyOn", (id, content) => Codec.StandbyActivate()); + AddAction("/standbyOff", (id, content) => Codec.StandbyDeactivate()); + } + catch (Exception e) + { + Debug.Console(2, this, "Error: {0}", e); + } + } + + private void SharingSourceFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + var state = new VideoCodecBaseStateMessage + { + SharingSource = e.StringValue + }; + + PostStatusMessage(state); + } + + private void SharingContentIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + var state = new VideoCodecBaseStateMessage + { + SharingContentIsOn = e.BoolValue + }; + + PostStatusMessage(state); + } + + private void PhonebookSyncState_InitialSyncCompleted(object sender, EventArgs e) + { + var state = new VideoCodecBaseStateMessage + { + InitialPhonebookSyncComplete = true + }; + + PostStatusMessage(state); + } + + private void CameraIsOffFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + PostCameraMode(); + } + + private void SelfviewIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + PostCameraSelfView(); + } + + private void PresetsCodec_CameraPresetsListHasChanged(object sender, EventArgs e) + { + PostCameraPresets(); + } + + private void CameraAutoModeIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + PostCameraMode(); + } + + + private void CameraCodec_CameraSelected(object sender, CameraSelectedEventArgs e) + { + MapCameraActions(); + PostSelectedCamera(); + } + + /// + /// Maps the camera control actions to the current selected camera on the codec + /// + private void MapCameraActions() + { + if (Codec is IHasCameras cameraCodec && cameraCodec.SelectedCamera != null) + { + RemoveAction("/cameraUp"); + RemoveAction("/cameraDown"); + RemoveAction("/cameraLeft"); + RemoveAction("/cameraRight"); + RemoveAction("/cameraZoomIn"); + RemoveAction("/cameraZoomOut"); + RemoveAction("/cameraHome"); + + if (cameraCodec.SelectedCamera is IHasCameraPtzControl camera) + { + AddAction("/cameraUp", (id, content) => HandleCameraPressAndHold(content, (b) => + { + if (b) + { + camera.TiltUp(); + return; + } + + camera.TiltStop(); + })); + + AddAction("/cameraDown", (id, content) => HandleCameraPressAndHold(content, (b) => + { + if (b) + { + camera.TiltDown(); + return; + } + + camera.TiltStop(); + })); + + AddAction("/cameraLeft", (id, content) => HandleCameraPressAndHold(content, (b) => + { + if (b) + { + camera.PanLeft(); + return; + } + + camera.PanStop(); + })); + + AddAction("/cameraRight", (id, content) => HandleCameraPressAndHold(content, (b) => + { + if (b) + { + camera.PanRight(); + return; + } + + camera.PanStop(); + })); + + AddAction("/cameraZoomIn", (id, content) => HandleCameraPressAndHold(content, (b) => + { + if (b) + { + camera.ZoomIn(); + return; + } + + camera.ZoomStop(); + })); + + AddAction("/cameraZoomOut", (id, content) => HandleCameraPressAndHold(content, (b) => + { + if (b) + { + camera.ZoomOut(); + return; + } + + camera.ZoomStop(); + })); + AddAction("/cameraHome", (id, content) => camera.PositionHome()); + + + RemoveAction("/cameraAutoFocus"); + RemoveAction("/cameraFocusNear"); + RemoveAction("/cameraFocusFar"); + + if (cameraCodec is IHasCameraFocusControl focusCamera) + { + AddAction("/cameraAutoFocus", (id, content) => focusCamera.TriggerAutoFocus()); + + AddAction("/cameraFocusNear", (id, content) => HandleCameraPressAndHold(content, (b) => + { + if (b) + { + focusCamera.FocusNear(); + return; + } + + focusCamera.FocusStop(); + })); + + AddAction("/cameraFocusFar", (id, content) => HandleCameraPressAndHold(content, (b) => + { + if (b) + { + focusCamera.FocusFar(); + return; + } + + focusCamera.FocusStop(); + })); + } + } + } + } + + 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 string GetCameraMode() + { + string m = ""; + + if (Codec is IHasCameraAutoMode speakerTrackCodec) + { + m = speakerTrackCodec.CameraAutoModeIsOnFeedback.BoolValue + ? eCameraControlMode.Auto.ToString().ToLower() + : eCameraControlMode.Manual.ToString().ToLower(); + } + + if (Codec is IHasCameraOff cameraOffCodec) + { + if (cameraOffCodec.CameraIsOffFeedback.BoolValue) + m = eCameraControlMode.Off.ToString().ToLower(); + } + + return m; + } + + private void PostCallHistory() + { + var codec = (Codec as IHasCallHistory); + + if (codec != null) + { + var status = new VideoCodecBaseStateMessage(); + + var recents = codec.CallHistory.RecentCalls; + + if (recents != null) + { + status.RecentCalls = codec.CallHistory.RecentCalls; + + PostStatusMessage(status); + } + } + } + + /// + /// Helper to grab a call with string ID + /// + /// + /// + private CodecActiveCallItem GetCallWithId(string id) + { + return Codec.ActiveCalls.FirstOrDefault(c => c.Id == id); + } + + /// + /// + /// + /// + private void GetDirectory(string id) + { + if (!(Codec is IHasDirectory dirCodec)) + { + return; + } + dirCodec.GetDirectoryFolderContents(id); + } + + /// + /// + /// + private void GetDirectoryRoot() + { + if (!(Codec is IHasDirectory dirCodec)) + { + // do something else? + return; + } + if (!dirCodec.PhonebookSyncState.InitialSyncComplete) + { + var state = new VideoCodecBaseStateMessage + { + InitialPhonebookSyncComplete = false + }; + + PostStatusMessage(state); + return; + } + + dirCodec.SetCurrentDirectoryToRoot(); + } + + /// + /// Requests the parent folder contents + /// + private void GetPreviousDirectory() + { + if (!(Codec is IHasDirectory dirCodec)) + { + return; + } + + dirCodec.GetDirectoryParentFolderContents(); + } + + /// + /// Handler for codec changes + /// + private void Codec_CallStatusChange(object sender, CodecCallStatusItemChangeEventArgs e) + { + SendFullStatus(); + } + + /// + /// + /// + private void SendIsReady() + { + var status = new VideoCodecBaseStateMessage(); + + var codecType = Codec.GetType(); + + status.IsReady = Codec.IsReady; + status.IsZoomRoom = codecType.GetInterface("IHasZoomRoomLayouts") != null; + + PostStatusMessage(status); + } + + /// + /// Helper method to build call status for vtc + /// + /// + protected VideoCodecBaseStateMessage GetStatus() + { + var status = new VideoCodecBaseStateMessage(); + + + if (Codec is IHasCodecCameras camerasCodec) + { + status.Cameras = new VideoCodecBaseStateMessage.CameraStatus + { + CameraManualIsSupported = true, + CameraAutoIsSupported = Codec.SupportsCameraAutoMode, + CameraOffIsSupported = Codec.SupportsCameraOff, + CameraMode = GetCameraMode(), + Cameras = camerasCodec.Cameras, + SelectedCamera = GetSelectedCamera(camerasCodec) + }; + } + + if (Codec is IHasDirectory directoryCodec) + { + status.HasDirectory = true; + status.HasDirectorySearch = true; + status.CurrentDirectory = directoryCodec.CurrentDirectoryResult; + } + + var codecType = Codec.GetType(); + + status.CameraSelfViewIsOn = Codec is IHasCodecSelfView && (Codec as IHasCodecSelfView).SelfviewIsOnFeedback.BoolValue; + status.IsInCall = Codec.IsInCall; + status.PrivacyModeIsOn = Codec.PrivacyModeIsOnFeedback.BoolValue; + status.SharingContentIsOn = Codec.SharingContentIsOnFeedback.BoolValue; + status.SharingSource = Codec.SharingSourceFeedback.StringValue; + status.StandbyIsOn = Codec.StandbyIsOnFeedback.BoolValue; + status.Calls = Codec.ActiveCalls; + status.Info = Codec.CodecInfo; + status.ShowSelfViewByDefault = Codec.ShowSelfViewByDefault; + status.SupportsAdHocMeeting = Codec is IHasStartMeeting; + status.HasRecents = Codec is IHasCallHistory; + status.HasCameras = Codec is IHasCameras; + status.Presets = GetCurrentPresets(); + status.IsZoomRoom = codecType.GetInterface("IHasZoomRoomLayouts") != null; + status.ReceivingContent = Codec is IHasFarEndContentStatus && (Codec as IHasFarEndContentStatus).ReceivingContent.BoolValue; + + if (Codec is IHasMeetingInfo meetingInfoCodec) + { + status.MeetingInfo = meetingInfoCodec.MeetingInfo; + } + + //Debug.Console(2, this, "VideoCodecBaseStatus:\n{0}", JsonConvert.SerializeObject(status)); + + return status; + } + + protected virtual void SendFullStatus() + { + if (!Codec.IsReady) + { + return; + } + + CrestronInvoke.BeginInvoke((o) => PostStatusMessage(GetStatus())); + } + + private void PostReceivingContent(bool receivingContent) + { + var state = new VideoCodecBaseStateMessage + { + ReceivingContent = receivingContent + }; + PostStatusMessage(state); + } + + private void PostCameraSelfView() + { + var status = new VideoCodecBaseStateMessage + { + CameraSelfViewIsOn = Codec is IHasCodecSelfView + && (Codec as IHasCodecSelfView).SelfviewIsOnFeedback.BoolValue + }; + + PostStatusMessage(status); + } + + /// + /// + /// + private void PostCameraMode() + { + var status = new VideoCodecBaseStateMessage + { + CameraMode = GetCameraMode() + }; + + PostStatusMessage(status); + } + + private void PostSelectedCamera() + { + var camerasCodec = Codec as IHasCodecCameras; + + var status = new VideoCodecBaseStateMessage + { + Cameras = new VideoCodecBaseStateMessage.CameraStatus() { SelectedCamera = GetSelectedCamera(camerasCodec) }, + Presets = GetCurrentPresets() + }; + PostStatusMessage(status); + } + + private void PostCameraPresets() + { + var status = new VideoCodecBaseStateMessage + { + Presets = GetCurrentPresets() + }; + + PostStatusMessage(status); + } + + private Camera GetSelectedCamera(IHasCodecCameras camerasCodec) + { + var camera = new Camera(); + + if (camerasCodec.SelectedCameraFeedback != null) + camera.Key = camerasCodec.SelectedCameraFeedback.StringValue; + if (camerasCodec.SelectedCamera != null) + { + camera.Name = camerasCodec.SelectedCamera.Name; + + camera.Capabilities = new Camera.CameraCapabilities() + { + CanPan = camerasCodec.SelectedCamera.CanPan, + CanTilt = camerasCodec.SelectedCamera.CanTilt, + CanZoom = camerasCodec.SelectedCamera.CanZoom, + CanFocus = camerasCodec.SelectedCamera.CanFocus, + }; + } + + if (camerasCodec.ControllingFarEndCameraFeedback != null) + camera.IsFarEnd = camerasCodec.ControllingFarEndCameraFeedback.BoolValue; + + + return camera; + } + + private List GetCurrentPresets() + { + var presetsCodec = Codec as IHasCodecRoomPresets; + + List currentPresets = null; + + if (presetsCodec != null && Codec is IHasFarEndCameraControl && + (Codec as IHasFarEndCameraControl).ControllingFarEndCameraFeedback.BoolValue) + currentPresets = presetsCodec.FarEndRoomPresets; + else if (presetsCodec != null) currentPresets = presetsCodec.NearEndPresets; + + return currentPresets; + } + } + + /// + /// A class that represents the state data to be sent to the user app + /// + public class VideoCodecBaseStateMessage : DeviceStateMessageBase + { + + [JsonProperty("calls", NullValueHandling = NullValueHandling.Ignore)] + public List Calls { get; set; } + + [JsonProperty("cameraMode", NullValueHandling = NullValueHandling.Ignore)] + public string CameraMode { get; set; } + + [JsonProperty("cameraSelfView", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraSelfViewIsOn { get; set; } + + [JsonProperty("cameras", NullValueHandling = NullValueHandling.Ignore)] + public CameraStatus Cameras { get; set; } + + [JsonProperty("cameraSupportsAutoMode", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraSupportsAutoMode { get; set; } + + [JsonProperty("cameraSupportsOffMode", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraSupportsOffMode { get; set; } + + [JsonProperty("currentDialString", NullValueHandling = NullValueHandling.Ignore)] + public string CurrentDialString { get; set; } + + [JsonProperty("currentDirectory", NullValueHandling = NullValueHandling.Ignore)] + public CodecDirectory CurrentDirectory { get; set; } + + [JsonProperty("directorySelectedFolderName", NullValueHandling = NullValueHandling.Ignore)] + public string DirectorySelectedFolderName { get; set; } + + [JsonProperty("hasCameras", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasCameras { get; set; } + + [JsonProperty("hasDirectory", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasDirectory { get; set; } + + [JsonProperty("hasDirectorySearch", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasDirectorySearch { get; set; } + + [JsonProperty("hasPresets", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasPresets { get; set; } + + [JsonProperty("hasRecents", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasRecents { get; set; } + + [JsonProperty("initialPhonebookSyncComplete", NullValueHandling = NullValueHandling.Ignore)] + public bool? InitialPhonebookSyncComplete { get; set; } + + [JsonProperty("info", NullValueHandling = NullValueHandling.Ignore)] + public VideoCodecInfo Info { get; set; } + + [JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsInCall { get; set; } + + [JsonProperty("isReady", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsReady { get; set; } + + [JsonProperty("isZoomRoom", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsZoomRoom { get; set; } + + [JsonProperty("meetingInfo", NullValueHandling = NullValueHandling.Ignore)] + public MeetingInfo MeetingInfo { get; set; } + + [JsonProperty("presets", NullValueHandling = NullValueHandling.Ignore)] + public List Presets { get; set; } + + [JsonProperty("privacyModeIsOn", NullValueHandling = NullValueHandling.Ignore)] + public bool? PrivacyModeIsOn { get; set; } + + [JsonProperty("receivingContent", NullValueHandling = NullValueHandling.Ignore)] + public bool? ReceivingContent { get; set; } + + [JsonProperty("recentCalls", NullValueHandling = NullValueHandling.Ignore)] + public List RecentCalls { get; set; } + + [JsonProperty("sharingContentIsOn", NullValueHandling = NullValueHandling.Ignore)] + public bool? SharingContentIsOn { get; set; } + + [JsonProperty("sharingSource", NullValueHandling = NullValueHandling.Ignore)] + public string SharingSource { get; set; } + + [JsonProperty("showCamerasWhenNotInCall", NullValueHandling = NullValueHandling.Ignore)] + public bool? ShowCamerasWhenNotInCall { get; set; } + + [JsonProperty("showSelfViewByDefault", NullValueHandling = NullValueHandling.Ignore)] + public bool? ShowSelfViewByDefault { get; set; } + + [JsonProperty("standbyIsOn", NullValueHandling = NullValueHandling.Ignore)] + public bool? StandbyIsOn { get; set; } + + [JsonProperty("supportsAdHocMeeting", NullValueHandling = NullValueHandling.Ignore)] + public bool? SupportsAdHocMeeting { get; set; } + + public class CameraStatus + { + [JsonProperty("cameraManualSupported", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraManualIsSupported { get; set; } + + [JsonProperty("cameraAutoSupported", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraAutoIsSupported { get; set; } + + [JsonProperty("cameraOffSupported", NullValueHandling = NullValueHandling.Ignore)] + public bool? CameraOffIsSupported { get; set; } + + [JsonProperty("cameraMode", NullValueHandling = NullValueHandling.Ignore)] + public string CameraMode { get; set; } + + [JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)] + public List Cameras { get; set; } + + [JsonProperty("selectedCamera", NullValueHandling = NullValueHandling.Ignore)] + public Camera SelectedCamera { get; set; } + + public class Camera + { + [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] + public string Key { get; set; } + + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + [JsonProperty("isFarEnd", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsFarEnd { get; set; } + + [JsonProperty("capabilities", NullValueHandling = NullValueHandling.Ignore)] + public CameraCapabilities Capabilities { get; set; } + + public class CameraCapabilities + { + [JsonProperty("canPan", NullValueHandling = NullValueHandling.Ignore)] + public bool? CanPan { get; set; } + + [JsonProperty("canTilt", NullValueHandling = NullValueHandling.Ignore)] + public bool? CanTilt { get; set; } + + [JsonProperty("canZoom", NullValueHandling = NullValueHandling.Ignore)] + public bool? CanZoom { get; set; } + + [JsonProperty("canFocus", NullValueHandling = NullValueHandling.Ignore)] + public bool? CanFocus { get; set; } + + } + } + + } + + } + + public class VideoCodecBaseEventMessage : DeviceEventMessageBase + { + + } + + public class PasswordPromptEventMessage : VideoCodecBaseEventMessage + { + [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] + public string Message { get; set; } + [JsonProperty("lastAttemptWasIncorrect", NullValueHandling = NullValueHandling.Ignore)] + public bool LastAttemptWasIncorrect { get; set; } + + [JsonProperty("loginAttemptFailed", NullValueHandling = NullValueHandling.Ignore)] + public bool LoginAttemptFailed { get; set; } + + [JsonProperty("loginAttemptCancelled", NullValueHandling = NullValueHandling.Ignore)] + public bool LoginAttemptCancelled { get; set; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs new file mode 100644 index 00000000..5e284df2 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; + +namespace PepperDash.Essentials.AppServer.Messengers +{ + +#if SERIES4 + public class MobileControlMessage : IMobileControlMessage +#else + public class MobileControlMessage +#endif + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("clientId")] + public string ClientId { get; set; } + + [JsonProperty("content")] + public JToken Content { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlSimpleContent.cs b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlSimpleContent.cs new file mode 100644 index 00000000..1d804758 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlSimpleContent.cs @@ -0,0 +1,10 @@ +using Newtonsoft.Json; + +namespace PepperDash.Essentials.AppServer +{ + public class MobileControlSimpleContent + { + [JsonProperty("value", NullValueHandling = NullValueHandling.Ignore)] + public T Value { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj new file mode 100644 index 00000000..fb9816ed --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -0,0 +1,48 @@ + + + ProgramLibrary + + + PepperDash.Essentials.AppServer + net472 + mobile-control-messengers + mobile-control-messengers + Copyright © 2024 + bin\$(Configuration)\ + false + true + $(Version) + false + PepperDash Technology + PepperDash.Essentials.Plugin.MobileControl.Messengers + https://github.com/PepperDash/Essentials + crestron 4series + + + full + $(DefineConstants);SERIES4 + + + pdbonly + $(DefineConstants);SERIES4 + + + + + + + + false + runtime + + + false + runtime + + + + + + + + \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/MobileControlSIMPLRoomJoinMap.cs b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/MobileControlSIMPLRoomJoinMap.cs new file mode 100644 index 00000000..fd6e8713 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/MobileControlSIMPLRoomJoinMap.cs @@ -0,0 +1,570 @@ +using PepperDash.Essentials.Core; + + +namespace PepperDash.Essentials.AppServer +{ + // ReSharper disable once InconsistentNaming + public class MobileControlSIMPLRoomJoinMap : JoinMapBaseAdvanced + { + [JoinName("QrCodeUrl")] + public JoinDataComplete QrCodeUrl = + new JoinDataComplete(new JoinData { JoinNumber = 403, JoinSpan = 1 }, + new JoinMetadata + { + Description = "QR Code URL", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("PortalSystemUrl")] + public JoinDataComplete PortalSystemUrl = + new JoinDataComplete(new JoinData { JoinNumber = 404, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Portal System URL", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("MasterVolume")] + public JoinDataComplete MasterVolume = + new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Master Volume Mute Toggle/FB/Level/Label", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.DigitalAnalogSerial + }); + + [JoinName("VolumeJoinStart")] + public JoinDataComplete VolumeJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 8 }, + new JoinMetadata + { + Description = "Volume Mute Toggle/FB/Level/Label", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.DigitalAnalogSerial + }); + + [JoinName("PrivacyMute")] + public JoinDataComplete PrivacyMute = + new JoinDataComplete(new JoinData { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Privacy Mute Toggle/FB", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("PromptForCode")] + public JoinDataComplete PromptForCode = + new JoinDataComplete(new JoinData { JoinNumber = 41, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Prompt User for Code", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ClientJoined")] + public JoinDataComplete ClientJoined = + new JoinDataComplete(new JoinData { JoinNumber = 42, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Client Joined", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ActivityPhoneCallEnable")] + public JoinDataComplete ActivityPhoneCallEnable = + new JoinDataComplete(new JoinData { JoinNumber = 48, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Enable Activity Phone Call", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ActivityVideoCallEnable")] + public JoinDataComplete ActivityVideoCallEnable = + new JoinDataComplete(new JoinData { JoinNumber = 49, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Enable Activity Video Call", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ActivityShare")] + public JoinDataComplete ActivityShare = + new JoinDataComplete(new JoinData { JoinNumber = 51, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Activity Share", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ActivityPhoneCall")] + public JoinDataComplete ActivityPhoneCall = + new JoinDataComplete(new JoinData { JoinNumber = 52, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Activity Phone Call", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ActivityVideoCall")] + public JoinDataComplete ActivityVideoCall = + new JoinDataComplete(new JoinData { JoinNumber = 53, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Activity Video Call", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ShutdownPromptDuration")] + public JoinDataComplete ShutdownPromptDuration = + new JoinDataComplete(new JoinData { JoinNumber = 61, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Shutdown Cancel", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Analog + }); + + [JoinName("ShutdownCancel")] + public JoinDataComplete ShutdownCancel = + new JoinDataComplete(new JoinData { JoinNumber = 61, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Shutdown Cancel", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ShutdownEnd")] + public JoinDataComplete ShutdownEnd = + new JoinDataComplete(new JoinData { JoinNumber = 62, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Shutdown End", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ShutdownStart")] + public JoinDataComplete ShutdownStart = + new JoinDataComplete(new JoinData { JoinNumber = 63, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Shutdown Start", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("SourceHasChanged")] + public JoinDataComplete SourceHasChanged = + new JoinDataComplete(new JoinData { JoinNumber = 71, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Source Changed", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CurrentSourceKey")] + public JoinDataComplete CurrentSourceKey = + new JoinDataComplete(new JoinData { JoinNumber = 71, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Key of selected source", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Serial + }); + + + [JoinName("ConfigIsLocal")] + public JoinDataComplete ConfigIsLocal = + new JoinDataComplete(new JoinData { JoinNumber = 100, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Config is local to Essentials", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("NumberOfAuxFaders")] + public JoinDataComplete NumberOfAuxFaders = + new JoinDataComplete(new JoinData { JoinNumber = 101, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Number of Auxilliary Faders", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Analog + }); + + [JoinName("SpeedDialNameStartJoin")] + public JoinDataComplete SpeedDialNameStartJoin = + new JoinDataComplete(new JoinData { JoinNumber = 241, JoinSpan = 10 }, + new JoinMetadata + { + Description = "Speed Dial names", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("SpeedDialNumberStartJoin")] + public JoinDataComplete SpeedDialNumberStartJoin = + new JoinDataComplete(new JoinData { JoinNumber = 251, JoinSpan = 10 }, + new JoinMetadata + { + Description = "Speed Dial numbers", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("SpeedDialVisibleStartJoin")] + public JoinDataComplete SpeedDialVisibleStartJoin = + new JoinDataComplete(new JoinData { JoinNumber = 261, JoinSpan = 10 }, + new JoinMetadata + { + Description = "Speed Dial Visible", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("RoomIsOn")] + public JoinDataComplete RoomIsOn = + new JoinDataComplete(new JoinData { JoinNumber = 301, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Room Is On", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("UserCodeToSystem")] + public JoinDataComplete UserCodeToSystem = + new JoinDataComplete(new JoinData { JoinNumber = 401, JoinSpan = 1 }, + new JoinMetadata + { + Description = "User Code", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("ServerUrl")] + public JoinDataComplete ServerUrl = + new JoinDataComplete(new JoinData { JoinNumber = 402, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Server URL", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("ConfigRoomName")] + public JoinDataComplete ConfigRoomName = + new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Room Name", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("ConfigHelpMessage")] + public JoinDataComplete ConfigHelpMessage = + new JoinDataComplete(new JoinData { JoinNumber = 502, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Room help message", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("ConfigHelpNumber")] + public JoinDataComplete ConfigHelpNumber = + new JoinDataComplete(new JoinData { JoinNumber = 503, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Room help number", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("ConfigRoomPhoneNumber")] + public JoinDataComplete ConfigRoomPhoneNumber = + new JoinDataComplete(new JoinData { JoinNumber = 504, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Room phone number", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("ConfigRoomURI")] + public JoinDataComplete ConfigRoomUri = + new JoinDataComplete(new JoinData { JoinNumber = 505, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Room URI", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("ApiOnlineAndAuthorized")] + public JoinDataComplete ApiOnlineAndAuthorized = + new JoinDataComplete(new JoinData { JoinNumber = 500, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Config info from SIMPL is ready", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ConfigIsReady")] + public JoinDataComplete ConfigIsReady = + new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Config info from SIMPL is ready", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ReadyForConfig")] + public JoinDataComplete ReadyForConfig = + new JoinDataComplete(new JoinData { JoinNumber = 501, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Config info from SIMPL is ready", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("HideVideoConfRecents")] + public JoinDataComplete HideVideoConfRecents = + new JoinDataComplete(new JoinData { JoinNumber = 502, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Hide Video Conference Recents", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("ShowCameraWhenNotInCall")] + public JoinDataComplete ShowCameraWhenNotInCall = + new JoinDataComplete(new JoinData { JoinNumber = 503, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Show camera when not in call", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("UseSourceEnabled")] + public JoinDataComplete UseSourceEnabled = + new JoinDataComplete(new JoinData { JoinNumber = 504, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Use Source Enabled Joins", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + + [JoinName("SourceShareDisableJoinStart")] + public JoinDataComplete SourceShareDisableJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 601, JoinSpan = 20 }, + new JoinMetadata + { + Description = "Source is not sharable", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("SourceIsEnabledJoinStart")] + public JoinDataComplete SourceIsEnabledJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 621, JoinSpan = 20 }, + new JoinMetadata + { + Description = "Source is enabled/visible", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("SourceIsControllableJoinStart")] + public JoinDataComplete SourceIsControllableJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 641, JoinSpan = 20 }, + new JoinMetadata + { + Description = "Source is controllable", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("SourceIsAudioSourceJoinStart")] + public JoinDataComplete SourceIsAudioSourceJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 661, JoinSpan = 20 }, + new JoinMetadata + { + Description = "Source is Audio Source", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + + [JoinName("SourceNameJoinStart")] + public JoinDataComplete SourceNameJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 601, JoinSpan = 20 }, + new JoinMetadata + { + Description = "Source Names", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("SourceIconJoinStart")] + public JoinDataComplete SourceIconJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 621, JoinSpan = 20 }, + new JoinMetadata + { + Description = "Source Icons", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("SourceKeyJoinStart")] + public JoinDataComplete SourceKeyJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 641, JoinSpan = 20 }, + new JoinMetadata + { + Description = "Source Keys", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("SourceControlDeviceKeyJoinStart")] + public JoinDataComplete SourceControlDeviceKeyJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 701, JoinSpan = 20 }, + new JoinMetadata + { + Description = "Source Control Device Keys", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("SourceTypeJoinStart")] + public JoinDataComplete SourceTypeJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 661, JoinSpan = 20 }, + new JoinMetadata + { + Description = "Source Types", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("CameraNearNameStart")] + public JoinDataComplete CameraNearNameStart = + new JoinDataComplete(new JoinData { JoinNumber = 761, JoinSpan = 10 }, + new JoinMetadata + { + Description = "Near End Camera Names", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("CameraFarName")] + public JoinDataComplete CameraFarName = + new JoinDataComplete(new JoinData { JoinNumber = 771, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Far End Camera Name", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + #region Advanced Sharing + [JoinName("SupportsAdvancedSharing")] + public JoinDataComplete SupportsAdvancedSharing = + new JoinDataComplete(new JoinData { JoinNumber = 505, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Supports Advanced Sharing", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("UseDestinationEnable")] + public JoinDataComplete UseDestinationEnable = + new JoinDataComplete(new JoinData { JoinNumber = 506, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Use Destination Enable", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + + [JoinName("UserCanChangeShareMode")] + public JoinDataComplete UserCanChangeShareMode = + new JoinDataComplete(new JoinData { JoinNumber = 507, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Share Mode Toggle Visible to User", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DestinationNameJoinStart")] + public JoinDataComplete DestinationNameJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 801, JoinSpan = 10 }, + new JoinMetadata + { + Description = "Destination Name", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("DestinationDeviceKeyJoinStart")] + public JoinDataComplete DestinationDeviceKeyJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 811, JoinSpan = 10 }, + new JoinMetadata + { + Description = "Destination Device Key", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("DestinationTypeJoinStart")] + public JoinDataComplete DestinationTypeJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 821, JoinSpan = 10 }, + new JoinMetadata + { + Description = "Destination type. Should be Audio, Video, AudioVideo", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("DestinationIsEnabledJoinStart")] + public JoinDataComplete DestinationIsEnabledJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 801, JoinSpan = 10 }, + new JoinMetadata + { + Description = "Show Destination on UI", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + #endregion + + public MobileControlSIMPLRoomJoinMap(uint joinStart) + : base(joinStart, typeof(MobileControlSIMPLRoomJoinMap)) + { + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/MobileControlSIMPLRunDirectRouteActionJoinMap.cs b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/MobileControlSIMPLRunDirectRouteActionJoinMap.cs new file mode 100644 index 00000000..10c516ee --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/MobileControlSIMPLRunDirectRouteActionJoinMap.cs @@ -0,0 +1,72 @@ +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.AppServer +{ + public class MobileControlSIMPLRunDirectRouteActionJoinMap : JoinMapBaseAdvanced + { + [JoinName("AdvancedSharingModeFb")] + public JoinDataComplete AdvancedSharingModeFb = + new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Use Advanced Sharing Mode", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("AdvancedSharingModeOn")] + public JoinDataComplete AdvancedSharingModeOn = + new JoinDataComplete(new JoinData { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Use Advanced Sharing Mode", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("AdvancedSharingModeOff")] + public JoinDataComplete AdvancedSharingModeOff = + new JoinDataComplete(new JoinData { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Use Advanced Sharing Mode", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("AdvancedSharingModeToggle")] + public JoinDataComplete AdvancedSharingModeToggle = + new JoinDataComplete(new JoinData { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Use Advanced Sharing Mode", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("SourceForDestinationJoinStart")] + public JoinDataComplete SourceForDestinationJoinStart = + new JoinDataComplete(new JoinData { JoinNumber = 51, JoinSpan = 10 }, + new JoinMetadata + { + Description = "Source to Route to Destination & FB", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("SourceForDestinationAudio")] + public JoinDataComplete SourceForDestinationAudio = + new JoinDataComplete(new JoinData { JoinNumber = 61, JoinSpan = 1 }, + new JoinMetadata + { + Description = "Source to Route to Destination & FB", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Serial + }); + + public MobileControlSIMPLRunDirectRouteActionJoinMap(uint joinStart) + : base(joinStart, typeof(MobileControlSIMPLRunDirectRouteActionJoinMap)) + { + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLAtcJoinMap.cs b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLAtcJoinMap.cs new file mode 100644 index 00000000..0ec4de5a --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLAtcJoinMap.cs @@ -0,0 +1,247 @@ +using PepperDash.Essentials.Core; + + +namespace PepperDash.Essentials.AppServer +{ + public class SIMPLAtcJoinMap : JoinMapBaseAdvanced + { + [JoinName("EndCall")] + public JoinDataComplete EndCall = + new JoinDataComplete(new JoinData() { JoinNumber = 21, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Hang Up", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("IncomingAnswer")] + public JoinDataComplete IncomingAnswer = + new JoinDataComplete(new JoinData() { JoinNumber = 51, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Answer Incoming Call", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("IncomingReject")] + public JoinDataComplete IncomingReject = + new JoinDataComplete(new JoinData() { JoinNumber = 52, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Reject Incoming Call", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("SpeedDialStart")] + public JoinDataComplete SpeedDialStart = + new JoinDataComplete(new JoinData() { JoinNumber = 41, JoinSpan = 4 }, + new JoinMetadata() + { + Description = "Speed Dial", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CurrentDialString")] + public JoinDataComplete CurrentDialString = + new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Dial String", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("CurrentCallNumber")] + public JoinDataComplete CurrentCallNumber = + new JoinDataComplete(new JoinData() { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Call Number", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("CurrentCallName")] + public JoinDataComplete CurrentCallName = + new JoinDataComplete(new JoinData() { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Call Name", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("HookState")] + public JoinDataComplete HookState = + new JoinDataComplete(new JoinData() { JoinNumber = 21, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Hook State", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("CallDirection")] + public JoinDataComplete CallDirection = + new JoinDataComplete(new JoinData() { JoinNumber = 22, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Call Direction", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("IncomingCallName")] + public JoinDataComplete IncomingCallName = + new JoinDataComplete(new JoinData() { JoinNumber = 51, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Incoming Call Name", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("IncomingCallNumber")] + public JoinDataComplete IncomingCallNumber = + new JoinDataComplete(new JoinData() { JoinNumber = 52, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Incoming Call Number", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("0")] + public JoinDataComplete Dtmf0 = + new JoinDataComplete(new JoinData() { JoinNumber = 10, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 0", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("1")] + public JoinDataComplete Dtmf1 = + new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 1", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("2")] + public JoinDataComplete Dtmf2 = + new JoinDataComplete(new JoinData() { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 2", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("3")] + public JoinDataComplete Dtmf3 = + new JoinDataComplete(new JoinData() { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 3", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("4")] + public JoinDataComplete Dtmf4 = + new JoinDataComplete(new JoinData() { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 4", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("5")] + public JoinDataComplete Dtmf5 = + new JoinDataComplete(new JoinData() { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 5", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("6")] + public JoinDataComplete Dtmf6 = + new JoinDataComplete(new JoinData() { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 6", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("7")] + public JoinDataComplete Dtmf7 = + new JoinDataComplete(new JoinData() { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 7", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("8")] + public JoinDataComplete Dtmf8 = + new JoinDataComplete(new JoinData() { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 8", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("9")] + public JoinDataComplete Dtmf9 = + new JoinDataComplete(new JoinData() { JoinNumber = 9, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 9", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("*")] + public JoinDataComplete DtmfStar = + new JoinDataComplete(new JoinData() { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF *", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("#")] + public JoinDataComplete DtmfPound = + new JoinDataComplete(new JoinData() { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF #", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + /// + /// Constructor that passes the joinStart to the base class + /// + /// + public SIMPLAtcJoinMap(uint joinStart) + : base(joinStart, typeof(SIMPLAtcJoinMap)) + { + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLVtcJoinMap.cs b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLVtcJoinMap.cs new file mode 100644 index 00000000..69b32495 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl.Messengers/SIMPLJoinMaps/SIMPLVtcJoinMap.cs @@ -0,0 +1,553 @@ +using PepperDash.Essentials.Core; + + +namespace PepperDash.Essentials.AppServer +{ + public class SIMPLVtcJoinMap : JoinMapBaseAdvanced + { + [JoinName("EndCall")] + public JoinDataComplete EndCall = + new JoinDataComplete(new JoinData() { JoinNumber = 24, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Hang Up", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("IncomingCall")] + public JoinDataComplete IncomingCall = + new JoinDataComplete(new JoinData() { JoinNumber = 50, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Incoming Call", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("IncomingAnswer")] + public JoinDataComplete IncomingAnswer = + new JoinDataComplete(new JoinData() { JoinNumber = 51, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Answer Incoming Call", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("IncomingReject")] + public JoinDataComplete IncomingReject = + new JoinDataComplete(new JoinData() { JoinNumber = 52, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Reject Incoming Call", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("SpeedDialStart")] + public JoinDataComplete SpeedDialStart = + new JoinDataComplete(new JoinData() { JoinNumber = 41, JoinSpan = 4 }, + new JoinMetadata() + { + Description = "Speed Dial", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DirectorySearchBusy")] + public JoinDataComplete DirectorySearchBusy = + new JoinDataComplete(new JoinData() { JoinNumber = 100, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Directory Search Busy FB", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DirectoryLineSelected")] + public JoinDataComplete DirectoryLineSelected = + new JoinDataComplete(new JoinData() { JoinNumber = 101, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Directory Line Selected FB", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DirectoryEntryIsContact")] + public JoinDataComplete DirectoryEntryIsContact = + new JoinDataComplete(new JoinData() { JoinNumber = 101, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Directory Selected Entry Is Contact FB", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DirectoryIsRoot")] + public JoinDataComplete DirectoryIsRoot = + new JoinDataComplete(new JoinData() { JoinNumber = 102, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Directory is on Root FB", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DDirectoryHasChanged")] + public JoinDataComplete DDirectoryHasChanged = + new JoinDataComplete(new JoinData() { JoinNumber = 103, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Directory has changed FB", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DirectoryRoot")] + public JoinDataComplete DirectoryRoot = + new JoinDataComplete(new JoinData() { JoinNumber = 104, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Go to Directory Root", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DirectoryFolderBack")] + public JoinDataComplete DirectoryFolderBack = + new JoinDataComplete(new JoinData() { JoinNumber = 105, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Go back one directory level", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DirectoryDialSelectedLine")] + public JoinDataComplete DirectoryDialSelectedLine = + new JoinDataComplete(new JoinData() { JoinNumber = 106, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Dial selected directory line", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraTiltUp")] + public JoinDataComplete CameraTiltUp = + new JoinDataComplete(new JoinData() { JoinNumber = 111, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Tilt Up", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraTiltDown")] + public JoinDataComplete CameraTiltDown = + new JoinDataComplete(new JoinData() { JoinNumber = 112, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Tilt Down", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraPanLeft")] + public JoinDataComplete CameraPanLeft = + new JoinDataComplete(new JoinData() { JoinNumber = 113, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Pan Left", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraPanRight")] + public JoinDataComplete CameraPanRight = + new JoinDataComplete(new JoinData() { JoinNumber = 114, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Pan Right", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraZoomIn")] + public JoinDataComplete CameraZoomIn = + new JoinDataComplete(new JoinData() { JoinNumber = 115, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Zoom In", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraZoomOut")] + public JoinDataComplete CameraZoomOut = + new JoinDataComplete(new JoinData() { JoinNumber = 116, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Zoom Out", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraPresetStart")] + public JoinDataComplete CameraPresetStart = + new JoinDataComplete(new JoinData() { JoinNumber = 121, JoinSpan = 5 }, + new JoinMetadata() + { + Description = "Camera Presets", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraModeAuto")] + public JoinDataComplete CameraModeAuto = + new JoinDataComplete(new JoinData() { JoinNumber = 131, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Mode Auto", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraModeManual")] + public JoinDataComplete CameraModeManual = + new JoinDataComplete(new JoinData() { JoinNumber = 132, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Mode Manual", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraModeOff")] + public JoinDataComplete CameraModeOff = + new JoinDataComplete(new JoinData() { JoinNumber = 133, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Mode Off", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraSelfView")] + public JoinDataComplete CameraSelfView = + new JoinDataComplete(new JoinData() { JoinNumber = 141, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Self View Toggle/FB", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraLayout")] + public JoinDataComplete CameraLayout = + new JoinDataComplete(new JoinData() { JoinNumber = 142, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Layout Toggle", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraSupportsAutoMode")] + public JoinDataComplete CameraSupportsAutoMode = + new JoinDataComplete(new JoinData() { JoinNumber = 143, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Supports Auto Mode FB", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraSupportsOffMode")] + public JoinDataComplete CameraSupportsOffMode = + new JoinDataComplete(new JoinData() { JoinNumber = 144, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Supports Off Mode FB", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("CameraNumberSelect")] + public JoinDataComplete CameraNumberSelect = + new JoinDataComplete(new JoinData() { JoinNumber = 60, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Camera Number Select/FB", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("DirectorySelectRow")] + public JoinDataComplete DirectorySelectRow = + new JoinDataComplete(new JoinData() { JoinNumber = 101, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Directory Select Row", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Analog + }); + + [JoinName("DirectoryRowCount")] + public JoinDataComplete DirectoryRowCount = + new JoinDataComplete(new JoinData() { JoinNumber = 101, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Directory Row Count FB", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Analog + }); + + [JoinName("CurrentDialString")] + public JoinDataComplete CurrentDialString = + new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Dial String", + JoinCapabilities = eJoinCapabilities.ToFromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("CurrentCallName")] + public JoinDataComplete CurrentCallName = + new JoinDataComplete(new JoinData() { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Call Name", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("CurrentCallNumber")] + public JoinDataComplete CurrentCallNumber = + new JoinDataComplete(new JoinData() { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Call Number", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("HookState")] + public JoinDataComplete HookState = + new JoinDataComplete(new JoinData() { JoinNumber = 31, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Hook State", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("CallDirection")] + public JoinDataComplete CallDirection = + new JoinDataComplete(new JoinData() { JoinNumber = 22, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Current Call Direction", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("IncomingCallName")] + public JoinDataComplete IncomingCallName = + new JoinDataComplete(new JoinData() { JoinNumber = 51, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Incoming Call Name", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("IncomingCallNumber")] + public JoinDataComplete IncomingCallNumber = + new JoinDataComplete(new JoinData() { JoinNumber = 52, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Incoming Call Number", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("DirectorySearchString")] + public JoinDataComplete DirectorySearchString = + new JoinDataComplete(new JoinData() { JoinNumber = 100, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Directory Search String", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("DirectoryEntriesStart")] + public JoinDataComplete DirectoryEntriesStart = + new JoinDataComplete(new JoinData() { JoinNumber = 101, JoinSpan = 255 }, + new JoinMetadata() + { + Description = "Directory Entries", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("DirectoryEntrySelectedName")] + public JoinDataComplete DirectoryEntrySelectedName = + new JoinDataComplete(new JoinData() { JoinNumber = 356, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Selected Directory Entry Name", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("DirectoryEntrySelectedNumber")] + public JoinDataComplete DirectoryEntrySelectedNumber = + new JoinDataComplete(new JoinData() { JoinNumber = 357, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Selected Directory Entry Number", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("DirectorySelectedFolderName")] + public JoinDataComplete DirectorySelectedFolderName = + new JoinDataComplete(new JoinData() { JoinNumber = 358, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "Selected Directory Folder Name", + JoinCapabilities = eJoinCapabilities.FromSIMPL, + JoinType = eJoinType.Serial + }); + + [JoinName("1")] + public JoinDataComplete Dtmf1 = + new JoinDataComplete(new JoinData() { JoinNumber = 1, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 1", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("2")] + public JoinDataComplete Dtmf2 = + new JoinDataComplete(new JoinData() { JoinNumber = 2, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 2", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("3")] + public JoinDataComplete Dtmf3 = + new JoinDataComplete(new JoinData() { JoinNumber = 3, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 3", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("4")] + public JoinDataComplete Dtmf4 = + new JoinDataComplete(new JoinData() { JoinNumber = 4, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 4", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("5")] + public JoinDataComplete Dtmf5 = + new JoinDataComplete(new JoinData() { JoinNumber = 5, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 5", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("6")] + public JoinDataComplete Dtmf6 = + new JoinDataComplete(new JoinData() { JoinNumber = 6, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 6", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("7")] + public JoinDataComplete Dtmf7 = + new JoinDataComplete(new JoinData() { JoinNumber = 7, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 7", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("8")] + public JoinDataComplete Dtmf8 = + new JoinDataComplete(new JoinData() { JoinNumber = 8, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 8", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("9")] + public JoinDataComplete Dtmf9 = + new JoinDataComplete(new JoinData() { JoinNumber = 9, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 9", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("0")] + public JoinDataComplete Dtmf0 = + new JoinDataComplete(new JoinData() { JoinNumber = 10, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF 0", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("*")] + public JoinDataComplete DtmfStar = + new JoinDataComplete(new JoinData() { JoinNumber = 11, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF *", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + [JoinName("#")] + public JoinDataComplete DtmfPound = + new JoinDataComplete(new JoinData() { JoinNumber = 12, JoinSpan = 1 }, + new JoinMetadata() + { + Description = "DTMF #", + JoinCapabilities = eJoinCapabilities.ToSIMPL, + JoinType = eJoinType.Digital + }); + + public SIMPLVtcJoinMap(uint joinStart) + : base(joinStart, typeof(SIMPLVtcJoinMap)) + { + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/AuthorizationResponse.cs b/src/PepperDash.Essentials.MobileControl/AuthorizationResponse.cs new file mode 100644 index 00000000..4e4a4439 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/AuthorizationResponse.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace PepperDash.Essentials +{ + public class AuthorizationResponse + { + [JsonProperty("authorized")] + public bool Authorized { get; set; } + + [JsonProperty("reason", NullValueHandling = NullValueHandling.Ignore)] + public string Reason { get; set; } = null; + } + + public class AuthorizationRequest + { + [JsonProperty("grantCode")] + public string GrantCode { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/Interfaces.cs b/src/PepperDash.Essentials.MobileControl/Interfaces.cs new file mode 100644 index 00000000..6b455e84 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Interfaces.cs @@ -0,0 +1,16 @@ +using System; + +namespace PepperDash.Essentials.Room.MobileControl +{ + /// + /// Represents a room whose configuration is derived from runtime data, + /// perhaps from another program, and that the data may not be fully + /// available at startup. + /// + public interface IDelayedConfiguration + { + + + event EventHandler ConfigurationIsReady; + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs new file mode 100644 index 00000000..ac09fbc3 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs @@ -0,0 +1,23 @@ +using Newtonsoft.Json.Linq; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.Web.RequestHandlers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PepperDash.Essentials +{ + public class MobileControlAction : IMobileControlAction + { + public IMobileControlMessenger Messenger { get; private set; } + + public Action Action {get; private set; } + + public MobileControlAction(IMobileControlMessenger messenger, Action handler) { + Messenger = messenger; + Action = handler; + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs new file mode 100644 index 00000000..f767830a --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs @@ -0,0 +1,161 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System; +using System.Collections.Generic; + +namespace PepperDash.Essentials +{ + /// + /// + /// + public class MobileControlConfig + { + [JsonProperty("serverUrl")] + public string ServerUrl { get; set; } + + [JsonProperty("clientAppUrl")] + public string ClientAppUrl { get; set; } + +#if SERIES4 + [JsonProperty("directServer")] + public MobileControlDirectServerPropertiesConfig DirectServer { get; set; } + + [JsonProperty("applicationConfig")] + public MobileControlApplicationConfig ApplicationConfig { get; set; } + + [JsonProperty("enableApiServer")] + public bool EnableApiServer { get; set; } +#endif + + [JsonProperty("roomBridges")] + [Obsolete("No longer necessary")] + public List RoomBridges { get; set; } + + public MobileControlConfig() + { + RoomBridges = new List(); + +#if SERIES4 + EnableApiServer = true; // default to true + ApplicationConfig = null; +#endif + } + } + + public class MobileControlDirectServerPropertiesConfig + { + [JsonProperty("enableDirectServer")] + public bool EnableDirectServer { get; set; } + + [JsonProperty("port")] + public int Port { get; set; } + + [JsonProperty("logging")] + public MobileControlLoggingConfig Logging { get; set; } + + [JsonProperty("automaticallyForwardPortToCSLAN")] + public bool? AutomaticallyForwardPortToCSLAN { get; set; } + + public MobileControlDirectServerPropertiesConfig() + { + Logging = new MobileControlLoggingConfig(); + } + } + + public class MobileControlLoggingConfig + { + [JsonProperty("enableRemoteLogging")] + public bool EnableRemoteLogging { get; set; } + + [JsonProperty("host")] + public string Host { get; set; } + + [JsonProperty("port")] + public int Port { get; set; } + + + + } + + public class MobileControlRoomBridgePropertiesConfig + { + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("roomKey")] + public string RoomKey { get; set; } + } + + /// + /// + /// + public class MobileControlSimplRoomBridgePropertiesConfig + { + [JsonProperty("eiscId")] + public string EiscId { get; set; } + } + + public class MobileControlApplicationConfig + { + [JsonProperty("apiPath")] + public string ApiPath { get; set; } + + [JsonProperty("gatewayAppPath")] + public string GatewayAppPath { get; set; } + + [JsonProperty("enableDev")] + public bool? EnableDev { get; set; } + + [JsonProperty("logoPath")] + /// + /// Client logo to be used in header and/or splash screen + /// + public string LogoPath { get; set; } + + [JsonProperty("iconSet")] + [JsonConverter(typeof(StringEnumConverter))] + public MCIconSet? IconSet { get; set; } + + [JsonProperty("loginMode")] + public string LoginMode { get; set; } + + [JsonProperty("modes")] + public Dictionary Modes { get; set; } + + [JsonProperty("enableRemoteLogging")] + public bool Logging { get; set; } + + [JsonProperty("partnerMetadata", NullValueHandling = NullValueHandling.Ignore)] + public List PartnerMetadata { get; set; } + } + + public class MobileControlPartnerMetadata + { + [JsonProperty("role")] + public string Role { get; set; } + + [JsonProperty("description")] + public string Description { get; set; } + + [JsonProperty("logoPath")] + public string LogoPath { get; set; } + } + + public class McMode + { + [JsonProperty("listPageText")] + public string ListPageText { get; set; } + [JsonProperty("loginHelpText")] + public string LoginHelpText { get; set; } + + [JsonProperty("passcodePageText")] + public string PasscodePageText { get; set; } + } + + public enum MCIconSet + { + GOOGLE, + HABANERO, + NEO + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs new file mode 100644 index 00000000..0d589c9f --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs @@ -0,0 +1,87 @@ +using PepperDash.Core; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Room.MobileControl; +using System; +using System.Collections.Generic; +using System.Linq; + + +namespace PepperDash.Essentials +{ + public class MobileControlDeviceFactory : EssentialsDeviceFactory + { + public MobileControlDeviceFactory() + { + TypeNames = new List { "appserver", "mobilecontrol", "webserver" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + try + { + var props = dc.Properties.ToObject(); + return new MobileControlSystemController(dc.Key, dc.Name, props); + } + catch (Exception e) + { + Debug.LogMessage(e, "Error building Mobile Control System Controller"); + return null; + } + } + } + + public class MobileControlSimplFactory : EssentialsDeviceFactory + { + public MobileControlSimplFactory() + { + TypeNames = new List { "mobilecontrolbridge-ddvc01", "mobilecontrolbridge-simpl" }; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + var comm = CommFactory.GetControlPropertiesConfig(dc); + + var bridge = new MobileControlSIMPLRoomBridge(dc.Key, dc.Name, comm.IpIdInt); + + bridge.AddPreActivationAction(() => + { + var parent = GetMobileControlDevice(); + + if (parent == null) + { + Debug.Console(0, bridge, "ERROR: Cannot connect bridge. System controller not present"); + return; + } + Debug.Console(0, bridge, "Linking to parent controller"); + + /*bridge.AddParent(parent); + parent.AddBridge(bridge);*/ + + parent.AddDeviceMessenger(bridge); + }); + + return bridge; + } + + private static MobileControlSystemController GetMobileControlDevice() + { + var mobileControlList = DeviceManager.AllDevices.OfType().ToList(); + + if (mobileControlList.Count > 1) + { + Debug.Console(0, Debug.ErrorLogLevel.Warning, + "Multiple instances of Mobile Control Server found."); + return null; + } + + if (mobileControlList.Count > 0) + { + return mobileControlList[0]; + } + + Debug.Console(0, Debug.ErrorLogLevel.Notice, "Mobile Control not enabled for this system"); + return null; + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs new file mode 100644 index 00000000..0ba33b6e --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MobileControlEssentialsConfig.cs @@ -0,0 +1,54 @@ +using Newtonsoft.Json; +using PepperDash.Essentials.Core.Config; +using System.Collections.Generic; + + +namespace PepperDash.Essentials +{ + /// + /// Used to overlay additional config data from mobile control on + /// + public class MobileControlEssentialsConfig : EssentialsConfig + { + [JsonProperty("runtimeInfo")] + public MobileControlRuntimeInfo RuntimeInfo { get; set; } + + public MobileControlEssentialsConfig(EssentialsConfig config) + : base() + { + // TODO: Consider using Reflection to iterate properties + this.Devices = config.Devices; + this.Info = config.Info; + this.JoinMaps = config.JoinMaps; + this.Rooms = config.Rooms; + this.SourceLists = config.SourceLists; + this.DestinationLists = config.DestinationLists; + this.SystemUrl = config.SystemUrl; + this.TemplateUrl = config.TemplateUrl; + this.TieLines = config.TieLines; + + if (this.Info == null) + this.Info = new InfoConfig(); + + RuntimeInfo = new MobileControlRuntimeInfo(); + } + } + + /// + /// Used to add any additional runtime information from mobile control to be send to the API + /// + public class MobileControlRuntimeInfo + { + [JsonProperty("pluginVersion")] + public string PluginVersion { get; set; } + + [JsonProperty("essentialsVersion")] + public string EssentialsVersion { get; set; } + + [JsonProperty("pepperDashCoreVersion")] + public string PepperDashCoreVersion { get; set; } + + [JsonProperty("essentialsPlugins")] + public List EssentialsPlugins { get; set; } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs new file mode 100644 index 00000000..e101dee8 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -0,0 +1,41 @@ +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace PepperDash.Essentials.MobileControl +{ + public class MobileControlFactory + { + public MobileControlFactory() { + var assembly = Assembly.GetExecutingAssembly(); + + PluginLoader.SetEssentialsAssembly(assembly.GetName().Name, assembly); + + var types = assembly.GetTypes().Where(t => typeof(IDeviceFactory).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); + + if(types == null) + { + return; + } + + foreach (var type in types) + { + try + { + var factory = (IDeviceFactory)Activator.CreateInstance(type); + + factory.LoadTypeFactories(); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Unable to load type '{type}' DeviceFactory: {factory}", null, type.Name); + } + } + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSimplDeviceBridge.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSimplDeviceBridge.cs new file mode 100644 index 00000000..f1ebf628 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSimplDeviceBridge.cs @@ -0,0 +1,143 @@ +using Crestron.SimplSharpPro.EthernetCommunication; +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System; + +namespace PepperDash.Essentials.Room.MobileControl +{ + /// + /// Represents a generic device connection through to and EISC for SIMPL01 + /// + public class MobileControlSimplDeviceBridge : Device, IChannel, INumericKeypad + { + /// + /// EISC used to talk to Simpl + /// + private readonly ThreeSeriesTcpIpEthernetIntersystemCommunications _eisc; + + public MobileControlSimplDeviceBridge(string key, string name, + ThreeSeriesTcpIpEthernetIntersystemCommunications eisc) + : base(key, name) + { + _eisc = eisc; + } + + #region IChannel Members + + public void ChannelUp(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void ChannelDown(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void LastChannel(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Guide(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Info(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Exit(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + #endregion + + #region INumericKeypad Members + + public void Digit0(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Digit1(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Digit2(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Digit3(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Digit4(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Digit5(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Digit6(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Digit7(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Digit8(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public void Digit9(bool pressRelease) + { + _eisc.SetBool(1111, pressRelease); + } + + public bool HasKeypadAccessoryButton1 + { + get { throw new NotImplementedException(); } + } + + public string KeypadAccessoryButton1Label + { + get { throw new NotImplementedException(); } + } + + public void KeypadAccessoryButton1(bool pressRelease) + { + throw new NotImplementedException(); + } + + public bool HasKeypadAccessoryButton2 + { + get { throw new NotImplementedException(); } + } + + public string KeypadAccessoryButton2Label + { + get { throw new NotImplementedException(); } + } + + public void KeypadAccessoryButton2(bool pressRelease) + { + throw new NotImplementedException(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs new file mode 100644 index 00000000..86d8f48d --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -0,0 +1,2674 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharp.Net.Http; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Org.BouncyCastle.Crypto.Prng; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.AppServer; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.CrestronIO; +using PepperDash.Essentials.Core.DeviceInfo; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.Lighting; +using PepperDash.Essentials.Core.Monitoring; +using PepperDash.Essentials.Core.Queues; +using PepperDash.Essentials.Core.Routing; +using PepperDash.Essentials.Core.Shades; +using PepperDash.Essentials.Core.Web; +using PepperDash.Essentials.Devices.Common.AudioCodec; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.SoftCodec; +using PepperDash.Essentials.Devices.Common.VideoCodec; +using PepperDash.Essentials.Room.MobileControl; +using PepperDash.Essentials.Services; +using PepperDash.Essentials.WebApiHandlers; +using Serilog.Events; +using WebSocketSharp; +using DisplayBase = PepperDash.Essentials.Devices.Common.Displays.DisplayBase; +using TwoWayDisplayBase = PepperDash.Essentials.Devices.Common.Displays.TwoWayDisplayBase; +#if SERIES4 +#endif + +namespace PepperDash.Essentials +{ + public class MobileControlSystemController : EssentialsDevice, IMobileControl + { + private bool _initialized = false; + private const long ServerReconnectInterval = 5000; + private const long PingInterval = 25000; + + private readonly Dictionary> _actionDictionary = + new Dictionary>( + StringComparer.InvariantCultureIgnoreCase + ); + + public Dictionary> ActionDictionary => _actionDictionary; + + private readonly GenericQueue _receiveQueue; + private readonly List _roomBridges = + new List(); + +#if SERIES4 + private readonly Dictionary _messengers = + new Dictionary(); + + private readonly Dictionary _defaultMessengers = + new Dictionary(); +#else + private readonly Dictionary _deviceMessengers = + new Dictionary(); +#endif + + private readonly GenericQueue _transmitToServerQueue; + + private readonly GenericQueue _transmitToClientsQueue; + + private bool _disableReconnect; + private WebSocket _wsClient2; + + public MobileControlApiService ApiService { get; private set; } + + public List RoomBridges => _roomBridges; + +#if SERIES4 + private readonly MobileControlWebsocketServer _directServer; + + public MobileControlWebsocketServer DirectServer => _directServer; +#endif + private readonly CCriticalSection _wsCriticalSection = new CCriticalSection(); + + public string SystemUrl; //set only from SIMPL Bridge! + + public bool Connected => _wsClient2 != null && _wsClient2.IsAlive; + + private IEssentialsRoomCombiner _roomCombiner; + + public string SystemUuid + { + get + { + // Check to see if the SystemUuid value is populated. If not populated from configuration, check for value from SIMPL bridge. + if ( + !string.IsNullOrEmpty(ConfigReader.ConfigObject.SystemUuid) + && ConfigReader.ConfigObject.SystemUuid != "missing url" + ) + { + return ConfigReader.ConfigObject.SystemUuid; + } + + Debug.Console( + 0, + this, + Debug.ErrorLogLevel.Notice, + "No system_url value defined in config. Checking for value from SIMPL Bridge." + ); + + if (!string.IsNullOrEmpty(SystemUrl)) + { + Debug.Console( + 0, + this, + Debug.ErrorLogLevel.Error, + "No system_url value defined in config or SIMPL Bridge. Unable to connect to Mobile Control." + ); + return string.Empty; + } + + var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*"); + string uuid = result.Groups[1].Value; + return uuid; + } + } + + public BoolFeedback ApiOnlineAndAuthorized { get; private set; } + + /// + /// Used for tracking HTTP debugging + /// + private bool _httpDebugEnabled; + + private bool _isAuthorized; + + /// + /// Tracks if the system is authorized to the API server + /// + public bool IsAuthorized + { + get { return _isAuthorized; } + private set + { + if (value == _isAuthorized) + return; + + _isAuthorized = value; + ApiOnlineAndAuthorized.FireUpdate(); + } + } + + private DateTime _lastAckMessage; + + public DateTime LastAckMessage => _lastAckMessage; + + private CTimer _pingTimer; + + private CTimer _serverReconnectTimer; + private LogLevel _wsLogLevel = LogLevel.Error; + + /// + /// + /// + /// + /// + /// + public MobileControlSystemController(string key, string name, MobileControlConfig config) + : base(key, name) + { + Config = config; + + // The queue that will collect the incoming messages in the order they are received + //_receiveQueue = new ReceiveQueue(key, ParseStreamRx); + _receiveQueue = new GenericQueue( + key + "-rxqueue", + Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority, + 25 + ); + + // The queue that will collect the outgoing messages in the order they are received + _transmitToServerQueue = new GenericQueue( + key + "-txqueue", + Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority, + 25 + ); + +#if SERIES4 + if (Config.DirectServer != null && Config.DirectServer.EnableDirectServer) + { + _directServer = new MobileControlWebsocketServer( + Key + "-directServer", + Config.DirectServer.Port, + this + ); + DeviceManager.AddDevice(_directServer); + + _transmitToClientsQueue = new GenericQueue( + key + "-clienttxqueue", + Crestron.SimplSharpPro.CrestronThread.Thread.eThreadPriority.HighPriority, + 25 + ); + } +#endif + + Host = config.ServerUrl; + if (!Host.StartsWith("http")) + { + Host = "https://" + Host; + } + + ApiService = new MobileControlApiService(Host); + + Debug.Console( + 0, + this, + "Mobile UI controller initializing for server:{0}", + config.ServerUrl + ); + + if (Global.Platform == eDevicePlatform.Appliance) + { + AddConsoleCommands(); + } + + AddPreActivationAction(() => LinkSystemMonitorToAppServer()); + + AddPreActivationAction(() => SetupDefaultDeviceMessengers()); + + AddPreActivationAction(() => SetupDefaultRoomMessengers()); + + AddPreActivationAction(() => AddWebApiPaths()); + + AddPreActivationAction(() => + { + _roomCombiner = DeviceManager.AllDevices.OfType().FirstOrDefault(); + + if(_roomCombiner == null) + return; + + _roomCombiner.RoomCombinationScenarioChanged += OnRoomCombinationScenarioChanged; + }); + + CrestronEnvironment.ProgramStatusEventHandler += + CrestronEnvironment_ProgramStatusEventHandler; + + ApiOnlineAndAuthorized = new BoolFeedback(() => + { + if (_wsClient2 == null) + return false; + + return _wsClient2.IsAlive && IsAuthorized; + }); + } + + private void SetupDefaultRoomMessengers() + { + Debug.LogMessage(LogEventLevel.Verbose, "Setting up room messengers", this); + foreach (var room in DeviceManager.AllDevices.OfType()) + { + Debug.LogMessage( + LogEventLevel.Verbose, + "Setting up room messengers for room: {key}", + this, + room.Key + ); + var messenger = new MobileControlEssentialsRoomBridge(room); + + messenger.AddParent(this); + + _roomBridges.Add(messenger); + + AddDefaultDeviceMessenger(messenger); + + Debug.LogMessage( + LogEventLevel.Verbose, + "Attempting to set up default room messengers for room: {0}", + this, + room.Key + ); + + if (room is IRoomEventSchedule) + { + Debug.LogMessage(LogEventLevel.Information, "Setting up event schedule messenger for room: {key}", this, room.Key); + + var scheduleMessenger = new RoomEventScheduleMessenger( + $"{room.Key}-schedule-{Key}", + string.Format("/room/{0}", room.Key), + room as IRoomEventSchedule + ); + + AddDefaultDeviceMessenger(scheduleMessenger); + } + + if (room is ITechPassword) + { + Debug.LogMessage(LogEventLevel.Information, "Setting up tech password messenger for room: {key}", this, room.Key); + + var techPasswordMessenger = new ITechPasswordMessenger( + $"{room.Key}-techPassword-{Key}", + string.Format("/room/{0}", room.Key), + room as ITechPassword + ); + + AddDefaultDeviceMessenger(techPasswordMessenger); + } + + if (room is IShutdownPromptTimer) + { + Debug.LogMessage(LogEventLevel.Information, "Setting up shutdown prompt timer messenger for room: {key}", this, room.Key); + + var shutdownPromptTimerMessenger = new IShutdownPromptTimerMessenger( + $"{room.Key}-shutdownPromptTimer-{Key}", + string.Format("/room/{0}", room.Key), + room as IShutdownPromptTimer + ); + + AddDefaultDeviceMessenger(shutdownPromptTimerMessenger); + } + + if (room is ILevelControls levelControls) + { + Debug.LogMessage(LogEventLevel.Information, "Setting up level controls messenger for room: {key}", this, room.Key); + + var levelControlsMessenger = new ILevelControlsMessenger( + $"{room.Key}-levelControls-{Key}", + $"/room/{room.Key}", + levelControls + ); + + AddDefaultDeviceMessenger(levelControlsMessenger); + } + } + } + + /// + /// Set up the messengers for each device type + /// + private void SetupDefaultDeviceMessengers() + { + bool messengerAdded = false; + var allDevices = DeviceManager.AllDevices.Where((d) => !(d is IEssentialsRoom)); + Debug.LogMessage( + LogEventLevel.Verbose, + "All Devices that aren't rooms count: {0}", + this, + allDevices?.Count() + ); + var count = allDevices.Count(); + foreach (var device in allDevices) + { + try + { + Debug.LogMessage( + LogEventLevel.Verbose, + "Attempting to set up device messengers for device: {0}", + this, + device.Key + ); + // StatusMonitorBase which is prop of ICommunicationMonitor is not a PepperDash.Core.Device, but is in the device array + if (device is ICommunicationMonitor) + { + Debug.LogMessage( + LogEventLevel.Verbose, + "Trying to cast to ICommunicationMonitor for device: {0}", + this, + device.Key + ); + var commMonitor = device as ICommunicationMonitor; + if (commMonitor == null) + { + Debug.LogMessage( + LogEventLevel.Debug, + "[Error] CommunicationMonitor cast is null for device: {0}. Skipping CommunicationMonitorMessenger", + this, + device.Key + ); + Debug.LogMessage( + LogEventLevel.Debug, + "AllDevices Completed a device. Devices Left: {0}", + this, + --count + ); + continue; + } + Debug.LogMessage( + LogEventLevel.Debug, + "Adding CommunicationMonitorMessenger for device: {0}", + this, + device.Key + ); + var commMessenger = new ICommunicationMonitorMessenger( + $"{device.Key}-commMonitor-{Key}", + string.Format("/device/{0}", device.Key), + commMonitor + ); + AddDefaultDeviceMessenger(commMessenger); + messengerAdded = true; + } + + if (device is CameraBase) + { + Debug.Console( + 2, + this, + "Adding CameraBaseMessenger for device: {0}", + device.Key + ); + + var cameraMessenger = new CameraBaseMessenger( + $"{device.Key}-cameraBase-{Key}", + device as CameraBase, + $"/device/{device.Key}" + ); + + AddDefaultDeviceMessenger(cameraMessenger); + + messengerAdded = true; + } + + if (device is BlueJeansPc) + { + Debug.Console( + 2, + this, + "Adding IRunRouteActionMessnger for device: {0}", + device.Key + ); + + var routeMessenger = new RunRouteActionMessenger( + $"{device.Key}-runRouteAction-{Key}", + device as BlueJeansPc, + $"/device/{device.Key}" + ); + + AddDefaultDeviceMessenger(routeMessenger); + + messengerAdded = true; + } + + if (device is ITvPresetsProvider) + { + Debug.LogMessage( + LogEventLevel.Verbose, + "Trying to cast to ITvPresetsProvider for device: {0}", + this, + device.Key + ); + + var presetsDevice = device as ITvPresetsProvider; + + if (presetsDevice.TvPresets == null) + { + Debug.Console( + 2, + this, + "TvPresets is null for device: '{0}'. Skipping DevicePresetsModelMessenger", + device.Key + ); + } + else + { + Debug.Console( + 2, + this, + "Adding ITvPresetsProvider for device: {0}", + device.Key + ); + + var presetsMessenger = new DevicePresetsModelMessenger( + $"{device.Key}-presets-{Key}", + $"/device/{device.Key}", + presetsDevice + ); + + AddDefaultDeviceMessenger(presetsMessenger); + + messengerAdded = true; + } + } + + if (device is DisplayBase) + { + Debug.Console(2, this, "Adding actions for device: {0}", device.Key); + + var dbMessenger = new DisplayBaseMessenger( + $"{device.Key}-displayBase-{Key}", + $"/device/{device.Key}", + device as DisplayBase + ); + + AddDefaultDeviceMessenger(dbMessenger); + + messengerAdded = true; + } + + if (device is Core.DisplayBase) + { + Debug.Console(2, this, "Adding actions for device: {0}", device.Key); + + var dbMessenger = new CoreDisplayBaseMessenger( + $"{device.Key}-displayBase-{Key}", + $"/device/{device.Key}", + device as Core.DisplayBase + ); + AddDefaultDeviceMessenger(dbMessenger); + + messengerAdded = true; + } + + if (device is TwoWayDisplayBase) + { + var display = device as TwoWayDisplayBase; + Debug.Console( + 2, + this, + "Adding TwoWayDisplayBase for device: {0}", + device.Key + ); + var twoWayDisplayMessenger = new TwoWayDisplayBaseMessenger( + $"{device.Key}-twoWayDisplay-{Key}", + string.Format("/device/{0}", device.Key), + display + ); + AddDefaultDeviceMessenger(twoWayDisplayMessenger); + + messengerAdded = true; + } + + if (device is Core.TwoWayDisplayBase) + { + var display = device as Core.TwoWayDisplayBase; + Debug.Console( + 2, + this, + "Adding TwoWayDisplayBase for device: {0}", + device.Key + ); + var twoWayDisplayMessenger = new CoreTwoWayDisplayBaseMessenger( + $"{device.Key}-twoWayDisplay-{Key}", + string.Format("/device/{0}", device.Key), + display + ); + AddDefaultDeviceMessenger(twoWayDisplayMessenger); + + messengerAdded = true; + } + + if (device is IBasicVolumeWithFeedback) + { + var deviceKey = device.Key; + Debug.Console( + 2, + this, + "Adding IBasicVolumeControlWithFeedback for device: {0}", + deviceKey + ); + var volControlDevice = device as IBasicVolumeWithFeedback; + var messenger = new DeviceVolumeMessenger( + $"{device.Key}-volume-{Key}", + string.Format("/device/{0}", deviceKey), + volControlDevice + ); + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is ILightingScenes) + { + var deviceKey = device.Key; + Debug.Console( + 2, + this, + "Adding LightingBaseMessenger for device: {0}", + deviceKey + ); + var lightingDevice = device as ILightingScenes; + var messenger = new ILightingScenesMessenger( + $"{device.Key}-lighting-{Key}", + lightingDevice, + string.Format("/device/{0}", deviceKey) + ); + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IShadesOpenCloseStop) + { + var deviceKey = device.Key; + var shadeDevice = device as IShadesOpenCloseStop; + Debug.Console( + 2, + this, + "Adding ShadeBaseMessenger for device: {0}", + deviceKey + ); + var messenger = new IShadesOpenCloseStopMessenger( + $"{device.Key}-shades-{Key}", + shadeDevice, + string.Format("/device/{0}", deviceKey) + ); + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is VideoCodecBase codec) + { + Debug.Console( + 2, + this, + $"Adding VideoCodecBaseMessenger for device: {codec.Key}" + ); + + var messenger = new VideoCodecBaseMessenger( + $"{codec.Key}-videoCodec-{Key}", + codec, + $"/device/{codec.Key}" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is AudioCodecBase audioCodec) + { + Debug.Console( + 2, + this, + $"Adding AudioCodecBaseMessenger for device: {audioCodec.Key}" + ); + + var messenger = new AudioCodecBaseMessenger( + $"{audioCodec.Key}-audioCodec-{Key}", + audioCodec, + $"/device/{audioCodec.Key}" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is ISetTopBoxControls) + { + Debug.Console( + 2, + this, + $"Adding ISetTopBoxControlMessenger for device: {device.Key}" + ); + + var dev = device as PepperDash.Core.Device; + + var messenger = new ISetTopBoxControlsMessenger( + $"{device.Key}-stb-{Key}", + $"/device/{device.Key}", + dev + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IChannel) + { + Debug.Console( + 2, + this, + $"Adding IChannelMessenger for device: {device.Key}" + ); + + var dev = device as PepperDash.Core.Device; + + var messenger = new IChannelMessenger( + $"{device.Key}-channel-{Key}", + $"/device/{device.Key}", + dev + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IColor) + { + Debug.Console(2, this, $"Adding IColorMessenger for device: {device.Key}"); + + var dev = device as PepperDash.Core.Device; + + var messenger = new IColorMessenger( + $"{device.Key}-color-{Key}", + $"/device/{device.Key}", + dev + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IDPad) + { + Debug.Console(2, this, $"Adding IDPadMessenger for device: {device.Key}"); + + var dev = device as PepperDash.Core.Device; + + var messenger = new IDPadMessenger( + $"{device.Key}-dPad-{Key}", + $"/device/{device.Key}", + dev + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is INumericKeypad) + { + Debug.Console( + 2, + this, + $"Adding INumericKeyapdMessenger for device: {device.Key}" + ); + + var dev = device as PepperDash.Core.Device; + + var messenger = new INumericKeypadMessenger( + $"{device.Key}-numericKeypad-{Key}", + $"/device/{device.Key}", + dev + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IHasPowerControl) + { + Debug.Console( + 2, + this, + $"Adding IHasPowerControlMessenger for device: {device.Key}" + ); + + var dev = device as PepperDash.Core.Device; + + var messenger = new IHasPowerMessenger( + $"{device.Key}-powerControl-{Key}", + $"/device/{device.Key}", + dev + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IHasPowerControlWithFeedback powerControl) + { + var deviceKey = device.Key; + Debug.Console( + 2, + this, + "Adding IHasPowerControlWithFeedbackMessenger for device: {0}", + deviceKey + ); + + var messenger = new IHasPowerControlWithFeedbackMessenger( + $"{device.Key}-powerFeedback-{Key}", + string.Format("/device/{0}", deviceKey), + powerControl + ); + AddDefaultDeviceMessenger(messenger); + messengerAdded = true; + } + + if (device is ITransport) + { + Debug.Console( + 2, + this, + $"Adding ITransportMessenger for device: {device.Key}" + ); + + var dev = device as PepperDash.Core.Device; + + var messenger = new IChannelMessenger( + $"{device.Key}-transport-{Key}", + $"/device/{device.Key}", + dev + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IHasCurrentSourceInfoChange) + { + Debug.Console( + 2, + this, + $"Adding IHasCurrentSourceInfoMessenger for device: {device.Key}" + ); + + var messenger = new IHasCurrentSourceInfoMessenger( + $"{device.Key}-currentSource-{Key}", + $"/device/{device.Key}", + device as IHasCurrentSourceInfoChange + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is ISwitchedOutput) + { + Debug.Console( + 2, + this, + $"Adding ISwitchedOutputMessenger for device: {device.Key}" + ); + + var messenger = new ISwitchedOutputMessenger( + $"{device.Key}-switchedOutput-{Key}", + device as ISwitchedOutput, + $"/device/{device.Key}" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IDeviceInfoProvider provider) + { + Debug.Console( + 2, + this, + $"Adding IHasDeviceInfoMessenger for device: {device.Key}" + ); + + var messenger = new DeviceInfoMessenger( + $"{device.Key}-deviceInfo-{Key}", + $"/device/{device.Key}", + provider + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is ILevelControls levelControls) + { + Debug.Console( + 2, + this, + $"Adding LevelControlsMessenger for device: {device.Key}" + ); + + var messenger = new ILevelControlsMessenger( + $"{device.Key}-levelControls-{Key}", + $"/device/{device.Key}", + levelControls + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + // This will work if TKey and TSelector are both string types. + // Otherwise plugin device needs to instantiate ISelectableItemsMessenger and add it to the controller. + if (device is IHasInputs inputs) + { + Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + + var messenger = new ISelectableItemsMessenger( + $"{device.Key}-inputs-{Key}", + $"/device/{device.Key}", + inputs.Inputs, + "inputs" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IHasInputs byteIntInputs) + { + Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + + var messenger = new ISelectableItemsMessenger( + $"{device.Key}-inputs-{Key}", + $"/device/{device.Key}", + byteIntInputs.Inputs, + "inputs" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IHasInputs stringInputs) + { + Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + + var messenger = new ISelectableItemsMessenger( + $"{device.Key}-inputs-{Key}", + $"/device/{device.Key}", + stringInputs.Inputs, + "inputs" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IHasInputs byteInputs) + { + Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + + var messenger = new ISelectableItemsMessenger( + $"{device.Key}-inputs-{Key}", + $"/device/{device.Key}", + byteInputs.Inputs, + "inputs" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IHasInputs intInputs) + { + Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + + var messenger = new ISelectableItemsMessenger( + $"{device.Key}-inputs-{Key}", + $"/device/{device.Key}", + intInputs.Inputs, + "inputs" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + + if (device is IMatrixRouting matrix) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Verbose, + "Adding IMatrixRoutingMessenger for device: {key}", + this, + device.Key + ); + + var messenger = new IMatrixRoutingMessenger( + $"{device.Key}-matrixRouting", + $"/device/{device.Key}", + matrix + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is ITemperatureSensor tempSensor) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Verbose, + "Adding ITemperatureSensor for device: {key}", + this, + device.Key + ); + + var messenger = new ITemperatureSensorMessenger( + $"{device.Key}-tempSensor", + tempSensor, + $"/device/{device.Key}" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IHumiditySensor humSensor) + { + Debug.LogMessage( + Serilog.Events.LogEventLevel.Verbose, + "Adding IHumiditySensor for device: {key}", + this, + device.Key + ); + + var messenger = new IHumiditySensorMessenger( + $"{device.Key}-humiditySensor", + humSensor, + $"/device/{device.Key}" + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IEssentialsRoomCombiner roomCombiner) + { + Debug.Console( + 2, + this, + $"Adding IEssentialsRoomCombinerMessenger for device: {device.Key}" + ); + + var messenger = new IEssentialsRoomCombinerMessenger( + $"{device.Key}-roomCombiner-{Key}", + $"/device/{device.Key}", + roomCombiner + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + if (device is IProjectorScreenLiftControl screenLiftControl) + { + Debug.Console( + 2, + this, + $"Adding IProjectorScreenLiftControlMessenger for device: {device.Key}" + ); + + var messenger = new IProjectorScreenLiftControlMessenger( + $"{device.Key}-screenLiftControl-{Key}", + $"/device/{device.Key}", + screenLiftControl + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + if (device is IDspPresets dspPresets) + { + Debug.Console( + 2, + this, + $"Adding IDspPresetsMessenger for device: {device.Key}" + ); + + var messenger = new IDspPresetsMessenger( + $"{device.Key}-dspPresets-{Key}", + $"/device/{device.Key}", + dspPresets + ); + + AddDefaultDeviceMessenger(messenger); + + messengerAdded = true; + } + + Debug.LogMessage( + LogEventLevel.Verbose, + "Trying to cast to generic device for device: {key}", + this, + device.Key + ); + if (device is EssentialsDevice) + { + var genericDevice = device as EssentialsDevice; + if (genericDevice == null || messengerAdded) + { + Debug.LogMessage( + LogEventLevel.Verbose, + "Skipping GenericMessenger for device: {0}. Messenger Added: {1}. GenericDevice null: {2}", + this, + device.Key, + messengerAdded, + genericDevice == null + ); + Debug.LogMessage( + LogEventLevel.Debug, + "AllDevices Completed a device. Devices Left: {0}", + this, + --count + ); + continue; + } + Debug.LogMessage( + LogEventLevel.Debug, + "Adding GenericMessenger for device: {0}", + this, + genericDevice?.Key + ); + AddDefaultDeviceMessenger( + new GenericMessenger( + genericDevice.Key + "-" + Key + "-generic", + genericDevice, + string.Format("/device/{0}", genericDevice.Key) + ) + ); + } + else + { + Debug.LogMessage( + LogEventLevel.Verbose, + "Not Essentials Device. Skipping GenericMessenger for device: {0}", + this, + device.Key + ); + } + Debug.LogMessage( + LogEventLevel.Debug, + "AllDevices Completed a device. Devices Left: {0}", + this, + --count + ); + } + catch (Exception ex) + { + Debug.LogMessage( + LogEventLevel.Verbose, + "[ERROR] setting up default device messengers: {0}", + this, + ex.Message + ); + Debug.LogMessage(ex, "[ERROR] setting up default device messengers", this); + } + } + } + + private void AddWebApiPaths() + { + var apiServer = DeviceManager + .AllDevices.OfType() + .FirstOrDefault(d => d.Key == "essentialsWebApi"); + + if (apiServer == null) + { + Debug.Console(0, this, "No API Server available"); + return; + } + + var routes = new List + { + new HttpCwsRoute($"device/{Key}/authorize") + { + Name = "MobileControlAuthorize", + RouteHandler = new MobileAuthRequestHandler(this) + }, + new HttpCwsRoute($"device/{Key}/info") + { + Name = "MobileControlInformation", + RouteHandler = new MobileInfoHandler(this) + }, + new HttpCwsRoute($"device/{Key}/actionPaths") + { + Name = "MobileControlActionPaths", + RouteHandler = new ActionPathsHandler(this) + } + }; + + apiServer.AddRoute(routes); + } + + private void AddConsoleCommands() + { + CrestronConsole.AddNewConsoleCommand( + AuthorizeSystem, + "mobileauth", + "Authorizes system to talk to Mobile Control server", + ConsoleAccessLevelEnum.AccessOperator + ); + CrestronConsole.AddNewConsoleCommand( + s => ShowInfo(), + "mobileinfo", + "Shows information for current mobile control session", + ConsoleAccessLevelEnum.AccessOperator + ); + CrestronConsole.AddNewConsoleCommand( + s => + { + s = s.Trim(); + if (!string.IsNullOrEmpty(s)) + { + _httpDebugEnabled = (s.Trim() != "0"); + } + CrestronConsole.ConsoleCommandResponse( + "HTTP Debug {0}", + _httpDebugEnabled ? "Enabled" : "Disabled" + ); + }, + "mobilehttpdebug", + "1 enables more verbose HTTP response debugging", + ConsoleAccessLevelEnum.AccessOperator + ); + CrestronConsole.AddNewConsoleCommand( + TestHttpRequest, + "mobilehttprequest", + "Tests an HTTP get to URL given", + ConsoleAccessLevelEnum.AccessOperator + ); + + CrestronConsole.AddNewConsoleCommand( + PrintActionDictionaryPaths, + "mobileshowactionpaths", + "Prints the paths in the Action Dictionary", + ConsoleAccessLevelEnum.AccessOperator + ); + CrestronConsole.AddNewConsoleCommand( + s => + { + _disableReconnect = false; + Debug.Console( + 1, + this, + Debug.ErrorLogLevel.Notice, + "User command: {0}", + "mobileConnect" + ); + ConnectWebsocketClient(); + }, + "mobileconnect", + "Forces connect of websocket", + ConsoleAccessLevelEnum.AccessOperator + ); + CrestronConsole.AddNewConsoleCommand( + s => + { + _disableReconnect = true; + Debug.Console( + 1, + this, + Debug.ErrorLogLevel.Notice, + "User command: {0}", + "mobileDisco" + ); + CleanUpWebsocketClient(); + }, + "mobiledisco", + "Disconnects websocket", + ConsoleAccessLevelEnum.AccessOperator + ); + + CrestronConsole.AddNewConsoleCommand( + ParseStreamRx, + "mobilesimulateaction", + "Simulates a message from the server", + ConsoleAccessLevelEnum.AccessOperator + ); + + CrestronConsole.AddNewConsoleCommand( + SetWebsocketDebugLevel, + "mobilewsdebug", + "Set Websocket debug level", + ConsoleAccessLevelEnum.AccessProgrammer + ); + } + + public MobileControlConfig Config { get; private set; } + + public string Host { get; private set; } + + public string ClientAppUrl => Config.ClientAppUrl; + + private void OnRoomCombinationScenarioChanged( + object sender, + EventArgs eventArgs + ) + { + SendMessageObject(new MobileControlMessage { Type = "/system/roomCombinationChanged" }); + } + + public bool CheckForDeviceMessenger(string key) + { + return _messengers.ContainsKey(key); + } + +#if SERIES4 + public void AddDeviceMessenger(IMobileControlMessenger messenger) +#else + public void AddDeviceMessenger(MessengerBase messenger) +#endif + { + if (_messengers.ContainsKey(messenger.Key)) + { + Debug.Console(1, this, "Messenger with key {0} already added", messenger.Key); + return; + } + + if (messenger is IDelayedConfiguration simplMessenger) + { + simplMessenger.ConfigurationIsReady += Bridge_ConfigurationIsReady; + } + + if (messenger is MobileControlBridgeBase roomBridge) + { + _roomBridges.Add(roomBridge); + } + + Debug.Console( + 2, + this, + "Adding messenger with key {0} for path {1}", + messenger.Key, + messenger.MessagePath + ); + + _messengers.Add(messenger.Key, messenger); + + messenger.RegisterWithAppServer(this); + } + + private void AddDefaultDeviceMessenger(IMobileControlMessenger messenger) + { + if (_defaultMessengers.ContainsKey(messenger.Key)) + { + Debug.Console( + 1, + this, + "Default messenger with key {0} already added", + messenger.Key + ); + return; + } + + if (messenger is IDelayedConfiguration simplMessenger) + { + simplMessenger.ConfigurationIsReady += Bridge_ConfigurationIsReady; + } + Debug.Console( + 2, + this, + "Adding default messenger with key {0} for path {1}", + messenger.Key, + messenger.MessagePath + ); + + _defaultMessengers.Add(messenger.Key, messenger); + + if (_initialized) + { + RegisterMessengerWithServer(messenger); + } + } + + private void RegisterMessengerWithServer(IMobileControlMessenger messenger) + { + Debug.Console( + 2, + this, + "Registering messenger with key {0} for path {1}", + messenger.Key, + messenger.MessagePath + ); + + messenger.RegisterWithAppServer(this); + } + + public override void Initialize() + { + foreach (var messenger in _messengers) + { + try + { + RegisterMessengerWithServer(messenger.Value); + } + catch (Exception ex) + { + Debug.Console( + 0, + this, + $"Exception registering paths for {messenger.Key}: {ex.Message}" + ); + Debug.Console( + 2, + this, + $"Exception registering paths for {messenger.Key}: {ex.StackTrace}" + ); + continue; + } + } + + foreach (var messenger in _defaultMessengers) + { + try + { + RegisterMessengerWithServer(messenger.Value); + } + catch (Exception ex) + { + Debug.Console( + 0, + this, + $"Exception registering paths for {messenger.Key}: {ex.Message}" + ); + Debug.Console( + 2, + this, + $"Exception registering paths for {messenger.Key}: {ex.StackTrace}" + ); + continue; + } + } + + var simplMessengers = _messengers.OfType().ToList(); + + if (simplMessengers.Count > 0) + { + return; + } + + _initialized = true; + + RegisterSystemToServer(); + } + + #region IMobileControl Members + + public static IMobileControl GetAppServer() + { + try + { + var appServer = + DeviceManager.GetDevices().SingleOrDefault(s => s is IMobileControl) + as MobileControlSystemController; + return appServer; + } + catch (Exception e) + { + Debug.Console(0, "Unable to find MobileControlSystemController in Devices: {0}", e); + return null; + } + } + + /// + /// Generates the url and creates the websocket client + /// + private bool CreateWebsocket() + { + if (_wsClient2 != null) + { + _wsClient2.Close(); + _wsClient2 = null; + } + + if (string.IsNullOrEmpty(SystemUuid)) + { + Debug.Console( + 0, + this, + Debug.ErrorLogLevel.Error, + "System UUID not defined. Unable to connect to Mobile Control" + ); + return false; + } + + var wsHost = Host.Replace("http", "ws"); + var url = string.Format("{0}/system/join/{1}", wsHost, SystemUuid); + + _wsClient2 = new WebSocket(url) + { + Log = + { + Output = (data, s) => + Debug.Console( + 1, + Debug.ErrorLogLevel.Notice, + "Message from websocket: {0}", + data + ) + } + }; + + _wsClient2.SslConfiguration.EnabledSslProtocols = + System.Security.Authentication.SslProtocols.Tls11 + | System.Security.Authentication.SslProtocols.Tls12; + + _wsClient2.OnMessage += HandleMessage; + _wsClient2.OnOpen += HandleOpen; + _wsClient2.OnError += HandleError; + _wsClient2.OnClose += HandleClose; + + return true; + } + + public void LinkSystemMonitorToAppServer() + { + if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Appliance) + { + Debug.Console( + 0, + this, + Debug.ErrorLogLevel.Notice, + "System Monitor does not exist for this platform. Skipping..." + ); + return; + } + + if (!(DeviceManager.GetDeviceForKey("systemMonitor") is SystemMonitorController sysMon)) + { + return; + } + + var key = sysMon.Key + "-" + Key; + var messenger = new SystemMonitorMessenger(key, sysMon, "/device/systemMonitor"); + + AddDeviceMessenger(messenger); + } + + /* public void CreateMobileControlRoomBridge(IEssentialsRoom room, IMobileControl parent) + { + var bridge = new MobileControlEssentialsRoomBridge(room); + AddBridgePostActivationAction(bridge); + DeviceManager.AddDevice(bridge); + } */ + + #endregion + + private void SetWebsocketDebugLevel(string cmdparameters) + { + if (CrestronEnvironment.ProgramCompatibility == eCrestronSeries.Series4) + { + Debug.Console( + 0, + this, + "Setting websocket log level not currently allowed on 4 series." + ); + return; // Web socket log level not currently allowed in series4 + } + + if (string.IsNullOrEmpty(cmdparameters)) + { + Debug.Console(0, this, "Current Websocket debug level: {0}", _wsLogLevel); + return; + } + + if (cmdparameters.ToLower().Contains("help") || cmdparameters.ToLower().Contains("?")) + { + Debug.Console( + 0, + this, + "valid options are:\r\n{0}\r\n{1}\r\n{2}\r\n{3}\r\n{4}\r\n{5}\r\n", + LogLevel.Trace, + LogLevel.Debug, + LogLevel.Info, + LogLevel.Warn, + LogLevel.Error, + LogLevel.Fatal + ); + } + + try + { + var debugLevel = (LogLevel)Enum.Parse(typeof(LogLevel), cmdparameters, true); + + _wsLogLevel = debugLevel; + + if (_wsClient2 != null) + { + _wsClient2.Log.Level = _wsLogLevel; + } + + Debug.Console(0, this, "Websocket log level set to {0}", debugLevel); + } + catch + { + Debug.Console( + 0, + this, + "{0} is not a valid debug level. Valid options are: {1}, {2}, {3}, {4}, {5}, {6}", + cmdparameters, + LogLevel.Trace, + LogLevel.Debug, + LogLevel.Info, + LogLevel.Warn, + LogLevel.Error, + LogLevel.Fatal + ); + } + } + + /* private void AddBridgePostActivationAction(MobileControlBridgeBase bridge) + { + bridge.AddPostActivationAction(() => + { + Debug.Console(0, bridge, "Linking to parent controller"); + bridge.AddParent(this); + AddBridge(bridge); + }); + }*/ + + /// + /// Sends message to server to indicate the system is shutting down + /// + /// + private void CrestronEnvironment_ProgramStatusEventHandler( + eProgramStatusEventType programEventType + ) + { + if ( + programEventType != eProgramStatusEventType.Stopping + || _wsClient2 == null + || !_wsClient2.IsAlive + ) + { + return; + } + + _disableReconnect = true; + + StopServerReconnectTimer(); + CleanUpWebsocketClient(); + } + + public void PrintActionDictionaryPaths(object o) + { + CrestronConsole.ConsoleCommandResponse("ActionDictionary Contents:\r\n"); + + foreach (var (messengerKey, actionPath) in GetActionDictionaryPaths()) + { + CrestronConsole.ConsoleCommandResponse($"<{messengerKey}> {actionPath}\r\n"); + } + } + + public List<(string, string)> GetActionDictionaryPaths() + { + var paths = new List<(string, string)>(); + + foreach (var item in _actionDictionary) + { + var messengers = item.Value.Select(a => a.Messenger).Cast(); + foreach (var messenger in messengers) + { + foreach (var actionPath in messenger.GetActionPaths()) + { + paths.Add((messenger.Key, $"{item.Key}{actionPath}")); + } + } + } + + return paths; + } + + /// + /// Adds an action to the dictionary + /// + /// The path of the API command + /// The action to be triggered by the commmand + public void AddAction(T messenger, Action action) + where T : IMobileControlMessenger + { + if ( + _actionDictionary.TryGetValue( + messenger.MessagePath, + out List actionList + ) + ) + { + if ( + actionList.Any(a => + a.Messenger.GetType() == messenger.GetType() + && a.Messenger.DeviceKey == messenger.DeviceKey + ) + ) + { + Debug.Console( + 0, + this, + $"Messenger of type {messenger.GetType().Name} already exists. Skipping actions for {messenger.Key}" + ); + return; + } + + actionList.Add(new MobileControlAction(messenger, action)); + return; + } + + actionList = new List + { + new MobileControlAction(messenger, action) + }; + + _actionDictionary.Add(messenger.MessagePath, actionList); + } + + /// + /// Removes an action from the dictionary + /// + /// + public void RemoveAction(string key) + { + if (_actionDictionary.ContainsKey(key)) + { + _actionDictionary.Remove(key); + } + } + + public MobileControlBridgeBase GetRoomBridge(string key) + { + return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key)); + } + + public IMobileControlRoomMessenger GetRoomMessenger(string key) + { + return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key)); + } + + /// + /// + /// + /// + /// + private void Bridge_ConfigurationIsReady(object sender, EventArgs e) + { + Debug.Console(1, this, "Bridge ready. Registering"); + + // send the configuration object to the server + + if (_wsClient2 == null) + { + RegisterSystemToServer(); + } + else if (!_wsClient2.IsAlive) + { + ConnectWebsocketClient(); + } + else + { + SendInitialMessage(); + } + } + + /// + /// + /// + /// + private void ReconnectToServerTimerCallback(object o) + { + Debug.Console(1, this, "Attempting to reconnect to server..."); + + ConnectWebsocketClient(); + } + + /// + /// Verifies system connection with servers + /// + private void AuthorizeSystem(string code) + { + if ( + string.IsNullOrEmpty(SystemUuid) + || SystemUuid.Equals("missing url", StringComparison.OrdinalIgnoreCase) + ) + { + CrestronConsole.ConsoleCommandResponse( + "System does not have a UUID. Please ensure proper configuration is loaded and restart." + ); + return; + } + if (string.IsNullOrEmpty(code)) + { + CrestronConsole.ConsoleCommandResponse( + "Please enter a grant code to authorize a system" + ); + return; + } + if (string.IsNullOrEmpty(Config.ServerUrl)) + { + CrestronConsole.ConsoleCommandResponse( + "Mobile control API address is not set. Check portal configuration" + ); + return; + } + + var authTask = ApiService.SendAuthorizationRequest(Host, code, SystemUuid); + + authTask.ContinueWith(t => + { + var response = t.Result; + + if (response.Authorized) + { + Debug.Console(0, this, "System authorized, sending config."); + RegisterSystemToServer(); + return; + } + + Debug.Console(0, this, response.Reason); + }); + } + + /// + /// Dumps info in response to console command. + /// + private void ShowInfo() + { + var url = Config != null ? Host : "No config"; + string name; + string code; + if (_roomBridges != null && _roomBridges.Count > 0) + { + name = _roomBridges[0].RoomName; + code = _roomBridges[0].UserCode; + } + else + { + name = "No config"; + code = "Not available"; + } + var conn = _wsClient2 == null ? "No client" : (_wsClient2.IsAlive ? "Yes" : "No"); + + var secSinceLastAck = DateTime.Now - _lastAckMessage; +#if SERIES4 + if (Config.EnableApiServer) + { +#endif + CrestronConsole.ConsoleCommandResponse( + @"Mobile Control Edge Server API Information: + + Server address: {0} + System Name: {1} + System URL: {2} + System UUID: {3} + System User code: {4} + Connected?: {5} + Seconds Since Last Ack: {6}", + url, + name, + ConfigReader.ConfigObject.SystemUrl, + SystemUuid, + code, + conn, + secSinceLastAck.Seconds + ); +#if SERIES4 + } + else + { + CrestronConsole.ConsoleCommandResponse( + @" +Mobile Control Edge Server API Information: + Not Enabled in Config. +" + ); + } + + if ( + Config.DirectServer != null + && Config.DirectServer.EnableDirectServer + && _directServer != null + ) + { + CrestronConsole.ConsoleCommandResponse( + @" +Mobile Control Direct Server Information: + User App URL: {0} + Server port: {1} +", + string.Format("{0}[insert_client_token]", _directServer.UserAppUrlPrefix), + _directServer.Port + ); + + CrestronConsole.ConsoleCommandResponse( + @" + UI Client Info: + Tokens Defined: {0} + Clients Connected: {1} +", + _directServer.UiClients.Count, + _directServer.ConnectedUiClientsCount + ); + + var clientNo = 1; + foreach (var clientContext in _directServer.UiClients) + { + var isAlive = false; + var duration = "Not Connected"; + + if (clientContext.Value.Client != null) + { + isAlive = clientContext.Value.Client.Context.WebSocket.IsAlive; + duration = clientContext.Value.Client.ConnectedDuration.ToString(); + } + + CrestronConsole.ConsoleCommandResponse( + @" +Client {0}: +Room Key: {1} +Touchpanel Key: {6} +Token: {2} +Client URL: {3} +Connected: {4} +Duration: {5} +", + clientNo, + clientContext.Value.Token.RoomKey, + clientContext.Key, + string.Format("{0}{1}", _directServer.UserAppUrlPrefix, clientContext.Key), + isAlive, + duration, + clientContext.Value.Token.TouchpanelKey + ); + clientNo++; + } + } + else + { + CrestronConsole.ConsoleCommandResponse( + @" +Mobile Control Direct Server Infromation: + Not Enabled in Config." + ); + } +#endif + } + + /// + /// Registers the room with the server + /// + public void RegisterSystemToServer() + { +#if SERIES4 + if (!Config.EnableApiServer) + { + Debug.Console( + 0, + this, + "ApiServer disabled via config. Cancelling attempt to register to server." + ); + return; + } +#endif + var result = CreateWebsocket(); + + if (!result) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Unable to create websocket."); + return; + } + + ConnectWebsocketClient(); + } + + /// + /// Connects the Websocket Client + /// + private void ConnectWebsocketClient() + { + try + { + _wsCriticalSection.Enter(); + + // set to 99999 to let things work on 4-Series + if ( + (CrestronEnvironment.ProgramCompatibility & eCrestronSeries.Series4) + == eCrestronSeries.Series4 + ) + { + _wsClient2.Log.Level = (LogLevel)99999; + } + else if ( + (CrestronEnvironment.ProgramCompatibility & eCrestronSeries.Series3) + == eCrestronSeries.Series3 + ) + { + _wsClient2.Log.Level = _wsLogLevel; + } + + //This version of the websocket client is TLS1.2 ONLY + + //Fires OnMessage event when PING is received. + _wsClient2.EmitOnPing = true; + + Debug.Console( + 1, + this, + Debug.ErrorLogLevel.Notice, + "Connecting mobile control client to {0}", + _wsClient2.Url + ); + + TryConnect(); + } + finally + { + _wsCriticalSection.Leave(); + } + } + + /// + /// Attempts to connect the websocket + /// + private void TryConnect() + { + try + { + IsAuthorized = false; + _wsClient2.Connect(); + } + catch (InvalidOperationException) + { + Debug.Console( + 0, + Debug.ErrorLogLevel.Error, + "Maximum retries exceeded. Restarting websocket" + ); + HandleConnectFailure(); + } + catch (IOException ex) + { + Debug.Console(0, this, Debug.ErrorLogLevel.Error, "IO Exception\r\n{0}", ex); + HandleConnectFailure(); + } + catch (Exception ex) + { + Debug.Console( + 0, + Debug.ErrorLogLevel.Error, + "Error on Websocket Connect: {0}\r\nStack Trace: {1}", + ex.Message, + ex.StackTrace + ); + HandleConnectFailure(); + } + } + + /// + /// Gracefully handles conect failures by reconstructing the ws client and starting the reconnect timer + /// + private void HandleConnectFailure() + { + _wsClient2 = null; + + var wsHost = Host.Replace("http", "ws"); + var url = string.Format("{0}/system/join/{1}", wsHost, SystemUuid); + _wsClient2 = new WebSocket(url) + { + Log = + { + Output = (data, s) => + Debug.Console( + 1, + Debug.ErrorLogLevel.Notice, + "Message from websocket: {0}", + data + ) + } + }; + + _wsClient2.OnMessage -= HandleMessage; + _wsClient2.OnOpen -= HandleOpen; + _wsClient2.OnError -= HandleError; + _wsClient2.OnClose -= HandleClose; + + _wsClient2.OnMessage += HandleMessage; + _wsClient2.OnOpen += HandleOpen; + _wsClient2.OnError += HandleError; + _wsClient2.OnClose += HandleClose; + + StartServerReconnectTimer(); + } + + /// + /// + /// + /// + /// + private void HandleOpen(object sender, EventArgs e) + { + StopServerReconnectTimer(); + StartPingTimer(); + Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Mobile Control API connected"); + SendMessageObject(new MobileControlMessage { Type = "hello" }); + } + + /// + /// + /// + /// + /// + private void HandleMessage(object sender, MessageEventArgs e) + { + if (e.IsPing) + { + _lastAckMessage = DateTime.Now; + IsAuthorized = true; + ResetPingTimer(); + return; + } + + if (e.IsText && e.Data.Length > 0) + { + _receiveQueue.Enqueue(new ProcessStringMessage(e.Data, ParseStreamRx)); + } + } + + /// + /// + /// + /// + /// + private void HandleError(object sender, ErrorEventArgs e) + { + Debug.Console(1, this, "Websocket error {0}", e.Message); + IsAuthorized = false; + StartServerReconnectTimer(); + } + + /// + /// + /// + /// + /// + private void HandleClose(object sender, CloseEventArgs e) + { + Debug.Console( + 1, + this, + Debug.ErrorLogLevel.Notice, + "Websocket close {0} {1}, clean={2}", + e.Code, + e.Reason, + e.WasClean + ); + IsAuthorized = false; + StopPingTimer(); + + // Start the reconnect timer only if disableReconnect is false and the code isn't 4200. 4200 indicates system is not authorized; + if (_disableReconnect || e.Code == 4200) + { + return; + } + + StartServerReconnectTimer(); + } + + /// + /// After a "hello" from the server, sends config and stuff + /// + private void SendInitialMessage() + { + Debug.Console(1, this, "Sending initial join message"); + + var touchPanels = DeviceManager + .AllDevices.OfType() + .Where(tp => !tp.UseDirectServer) + .Select( + (tp) => + { + return new { touchPanelKey = tp.Key, roomKey = tp.DefaultRoomKey }; + } + ); + + var msg = new MobileControlMessage + { + Type = "join", + Content = JToken.FromObject( + new { config = GetConfigWithPluginVersion(), touchPanels } + ) + }; + + SendMessageObject(msg); + } + + public MobileControlEssentialsConfig GetConfigWithPluginVersion() + { + // Populate the application name and version number + var confObject = new MobileControlEssentialsConfig(ConfigReader.ConfigObject); + + confObject.Info.RuntimeInfo.AppName = Assembly.GetExecutingAssembly().GetName().Name; + + var essentialsVersion = Global.AssemblyVersion; + confObject.Info.RuntimeInfo.AssemblyVersion = essentialsVersion; + +//#if DEBUG +// // Set for local testing +// confObject.RuntimeInfo.PluginVersion = "4.0.0-localBuild"; +//#else + // Populate the plugin version + var pluginVersion = Assembly + .GetExecutingAssembly() + .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); + + var fullVersionAtt = pluginVersion[0] as AssemblyInformationalVersionAttribute; + + if (fullVersionAtt != null) + { + var pluginInformationalVersion = fullVersionAtt.InformationalVersion; + + confObject.RuntimeInfo.PluginVersion = pluginInformationalVersion; + confObject.RuntimeInfo.EssentialsVersion = Global.AssemblyVersion; + confObject.RuntimeInfo.PepperDashCoreVersion = PluginLoader.PepperDashCoreAssembly.Version; + confObject.RuntimeInfo.EssentialsPlugins = PluginLoader.EssentialsPluginAssemblies; + } +//#endif + return confObject; + } + + public void SetClientUrl(string path, string roomKey = null) + { + var message = new MobileControlMessage + { + Type = string.IsNullOrEmpty(roomKey) ? $"/event/system/setUrl" : $"/event/room/{roomKey}/setUrl", + Content = JToken.FromObject(new MobileControlSimpleContent { Value = path }) + }; + + SendMessageObject(message); + } + + /// + /// Sends any object type to server + /// + /// + public void SendMessageObject(IMobileControlMessage o) + { +#if SERIES4 + if (Config.EnableApiServer) + { +#endif + _transmitToServerQueue.Enqueue(new TransmitMessage(o, _wsClient2)); +#if SERIES4 + } + + if ( + Config.DirectServer != null + && Config.DirectServer.EnableDirectServer + && _directServer != null + ) + { + _transmitToClientsQueue.Enqueue(new MessageToClients(o, _directServer)); + } +#endif + } + +#if SERIES4 + public void SendMessageObjectToDirectClient(object o) + { + if ( + Config.DirectServer != null + && Config.DirectServer.EnableDirectServer + && _directServer != null + ) + { + _transmitToClientsQueue.Enqueue(new MessageToClients(o, _directServer)); + } + } +#endif + + /// + /// Disconnects the Websocket Client and stops the heartbeat timer + /// + private void CleanUpWebsocketClient() + { + if (_wsClient2 == null) + { + return; + } + + Debug.Console(1, this, "Disconnecting websocket"); + + _wsClient2.Close(); + } + + private void ResetPingTimer() + { + // This tells us we're online with the API and getting pings + _pingTimer.Reset(PingInterval); + } + + private void StartPingTimer() + { + StopPingTimer(); + _pingTimer = new CTimer(PingTimerCallback, null, PingInterval); + } + + private void StopPingTimer() + { + if (_pingTimer == null) + { + return; + } + + _pingTimer.Stop(); + _pingTimer.Dispose(); + _pingTimer = null; + } + + private void PingTimerCallback(object o) + { + Debug.Console( + 1, + this, + Debug.ErrorLogLevel.Notice, + "Ping timer expired. Closing websocket" + ); + + try + { + _wsClient2.Close(); + } + catch (Exception ex) + { + Debug.Console( + 0, + Debug.ErrorLogLevel.Error, + "Exception closing websocket: {0}\r\nStack Trace: {1}", + ex.Message, + ex.StackTrace + ); + + HandleConnectFailure(); + } + } + + /// + /// + /// + private void StartServerReconnectTimer() + { + StopServerReconnectTimer(); + _serverReconnectTimer = new CTimer( + ReconnectToServerTimerCallback, + ServerReconnectInterval + ); + Debug.Console(1, this, "Reconnect Timer Started."); + } + + /// + /// Does what it says + /// + private void StopServerReconnectTimer() + { + if (_serverReconnectTimer == null) + { + return; + } + _serverReconnectTimer.Stop(); + _serverReconnectTimer = null; + } + + /// + /// Resets reconnect timer and updates usercode + /// + /// + private void HandleHeartBeat(JToken content) + { + SendMessageObject(new MobileControlMessage { Type = "/system/heartbeatAck" }); + + var code = content["userCode"]; + if (code == null) + { + return; + } + + foreach (var b in _roomBridges) + { + b.SetUserCode(code.Value()); + } + } + + //private void HandleClientJoined(JToken content) + //{ + // var clientId = content["clientId"].Value(); + // var roomKey = content["roomKey"].Value(); + + // SendMessageObject( + // new MobileControlMessage + // { + // Type = "/system/roomKey", + // ClientId = clientId, + // Content = roomKey + // } + // ); + //} + + private void HandleClientJoined(JToken content) + { + var clientId = content["clientId"].Value(); + var roomKey = content["roomKey"].Value(); + var touchpanelKey = content.SelectToken("touchpanelKey"); //content["touchpanelKey"].Value(); + + if (_roomCombiner == null) + { + var message = new MobileControlMessage + { + Type = "/system/roomKey", + ClientId = clientId, + Content = roomKey + }; + + SendMessageObject(message); + return; + } + + if (!_roomCombiner.CurrentScenario.UiMap.ContainsKey(roomKey)) + { + Debug.Console(0, this, + "Unable to find correct roomKey for {0} in current scenario. Returning {0} as roomKey", roomKey); + + var message = new MobileControlMessage + { + Type = "/system/roomKey", + ClientId = clientId, + Content = roomKey + }; + + SendMessageObject(message); + return; + } + + var newRoomKey = _roomCombiner.CurrentScenario.UiMap[roomKey]; + + var newMessage = new MobileControlMessage + { + Type = "/system/roomKey", + ClientId = clientId, + Content = newRoomKey + }; + + SendMessageObject(newMessage); + } + + private void HandleUserCode(JToken content, Action action = null) + { + var code = content["userCode"]; + + JToken qrChecksum; + + try + { + qrChecksum = content.SelectToken("qrChecksum", false); + } + catch + { + qrChecksum = new JValue(string.Empty); + } + + Debug.Console( + 1, + this, + "QR checksum: {0}", + qrChecksum == null ? string.Empty : qrChecksum.Value() + ); + + if (code == null) + { + return; + } + + if (action == null) + { + foreach (var bridge in _roomBridges) + { + bridge.SetUserCode(code.Value(), qrChecksum.Value()); + } + + return; + } + + action(code.Value(), qrChecksum.Value()); + } + + public void HandleClientMessage(string message) + { + _receiveQueue.Enqueue(new ProcessStringMessage(message, ParseStreamRx)); + } + + /// + /// + /// + private void ParseStreamRx(string messageText) + { + if (string.IsNullOrEmpty(messageText)) + { + return; + } + + if (!messageText.Contains("/system/heartbeat")) + { + Debug.LogMessage( + LogEventLevel.Debug, + "Message RX: {messageText}", + this, + messageText + ); + } + + try + { + var message = JsonConvert.DeserializeObject(messageText); + + switch (message.Type) + { + case "hello": + SendInitialMessage(); + break; + case "/system/heartbeat": + HandleHeartBeat(message.Content); + break; + case "/system/userCode": + HandleUserCode(message.Content); + break; + case "/system/clientJoined": + HandleClientJoined(message.Content); + break; + case "/system/reboot": + SystemMonitorController.ProcessorReboot(); + break; + case "/system/programReset": + SystemMonitorController.ProgramReset(InitialParametersClass.ApplicationNumber); + break; + case "raw": + var wrapper = message.Content.ToObject(); + DeviceJsonApi.DoDeviceAction(wrapper); + break; + case "close": + Debug.Console(1, this, "Received close message from server."); + break; + default: + // Incoming message example + // /room/roomA/status + // /room/roomAB/status + + // ActionDictionary Keys example + // /room/roomA + // /room/roomAB + + // Can't do direct comparison because it will match /room/roomA with /room/roomA/xxx instead of /room/roomAB/xxx + var handlersKv = _actionDictionary.FirstOrDefault(kv => message.Type.StartsWith(kv.Key + "/")); // adds trailing slash to ensure above case is handled + + + if (handlersKv.Key == null) + { + this.LogInformation("-- Warning: Incoming message has no registered handler {type}", message.Type); + break; + } + + var handlers = handlersKv.Value; + + foreach (var handler in handlers) + { + Task.Run( + () => + handler.Action(message.Type, message.ClientId, message.Content) + ); + } + + break; + } + } + catch (Exception err) + { + Debug.LogMessage( + err, + "Unable to parse {message}", + this, + messageText + ); + } + } + + /// + /// + /// + /// + private void TestHttpRequest(string s) + { + { + s = s.Trim(); + if (string.IsNullOrEmpty(s)) + { + PrintTestHttpRequestUsage(); + return; + } + var tokens = s.Split(' '); + if (tokens.Length < 2) + { + CrestronConsole.ConsoleCommandResponse("Too few paramaters\r"); + PrintTestHttpRequestUsage(); + return; + } + + try + { + var url = tokens[1]; + switch (tokens[0].ToLower()) + { + case "get": + { + var resp = new HttpClient().Get(url); + CrestronConsole.ConsoleCommandResponse("RESPONSE:\r{0}\r\r", resp); + } + break; + case "post": + { + var resp = new HttpClient().Post(url, new byte[] { }); + CrestronConsole.ConsoleCommandResponse("RESPONSE:\r{0}\r\r", resp); + } + break; + default: + CrestronConsole.ConsoleCommandResponse("Only get or post supported\r"); + PrintTestHttpRequestUsage(); + break; + } + } + catch (HttpException e) + { + CrestronConsole.ConsoleCommandResponse("Exception in request:\r"); + CrestronConsole.ConsoleCommandResponse( + "Response URL: {0}\r", + e.Response.ResponseUrl + ); + CrestronConsole.ConsoleCommandResponse( + "Response Error Code: {0}\r", + e.Response.Code + ); + CrestronConsole.ConsoleCommandResponse( + "Response body: {0}\r", + e.Response.ContentString + ); + } + } + } + + private void PrintTestHttpRequestUsage() + { + CrestronConsole.ConsoleCommandResponse("Usage: mobilehttprequest:N get/post url\r"); + } + } + + public class ClientSpecificUpdateRequest + { + public ClientSpecificUpdateRequest(Action action) + { + ResponseMethod = action; + } + + public Action ResponseMethod { get; private set; } + } + + public class UserCodeChanged + { + public Action UpdateUserCode { get; private set; } + + public UserCodeChanged(Action updateMethod) + { + UpdateUserCode = updateMethod; + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj new file mode 100644 index 00000000..054676e7 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -0,0 +1,67 @@ + + + ProgramLibrary + + + PepperDash.Essentials + net472 + true + false + epi-essentials-mobile-control + PepperDash Technologies + epi-essentials-mobile-control + This software is a plugin designed to work as a part of PepperDash Essentials for Crestron control processors. This plugin allows for connection to a PepperDash Mobile Control server. + Copyright 2020 + 4.0.0-local + true + $(Version) + false + bin\$(Configuration)\ + PepperDash Technologies + PepperDash.Essentials.4Series.Plugin.MobileControl + https://github.com/PepperDash/Essentials + crestron 4series + + + TRACE;DEBUG;SERIES4 + + + pdbonly + TRACE;SERIES4 + + + + + + + + + + + + + + + + + + + + + + + + + false + runtime + + + false + runtime + + + false + runtime + + + \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs new file mode 100644 index 00000000..e46eaf34 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs @@ -0,0 +1,130 @@ +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using System; + + +namespace PepperDash.Essentials +{ + /// + /// + /// + public abstract class MobileControlBridgeBase : MessengerBase, IMobileControlRoomMessenger + { + public event EventHandler UserCodeChanged; + + public event EventHandler UserPromptedForCode; + + public event EventHandler ClientJoined; + + public event EventHandler AppUrlChanged; + + public IMobileControl Parent { get; private set; } + + public string AppUrl { get; private set; } + public string UserCode { get; private set; } + + public string QrCodeUrl { get; protected set; } + + public string QrCodeChecksum { get; protected set; } + + public string McServerUrl { get; private set; } + + public abstract string RoomName { get; } + + public abstract string RoomKey { get; } + + protected MobileControlBridgeBase(string key, string messagePath) + : base(key, messagePath) + { + } + + protected MobileControlBridgeBase(string key, string messagePath, IKeyName device) + : base(key, messagePath, device) + { + } + + /// + /// Set the parent. Does nothing else. Override to add functionality such + /// as adding actions to parent + /// + /// + public virtual void AddParent(IMobileControl parent) + { + Parent = parent; + + McServerUrl = Parent.ClientAppUrl; + } + + /// + /// Sets the UserCode on the bridge object. Called from controller. A changed code will + /// fire method UserCodeChange. Override that to handle changes + /// + /// + public void SetUserCode(string code) + { + var changed = UserCode != code; + UserCode = code; + if (changed) + { + UserCodeChange(); + } + } + + + /// + /// Sets the UserCode on the bridge object. Called from controller. A changed code will + /// fire method UserCodeChange. Override that to handle changes + /// + /// + /// Checksum of the QR code. Used for Cisco codec branding command + public void SetUserCode(string code, string qrChecksum) + { + QrCodeChecksum = qrChecksum; + + SetUserCode(code); + } + + public virtual void UpdateAppUrl(string url) + { + AppUrl = url; + + var handler = AppUrlChanged; + + if (handler == null) return; + + handler(this, new EventArgs()); + } + + /// + /// Empty method in base class. Override this to add functionality + /// when code changes + /// + protected virtual void UserCodeChange() + { + Debug.Console(1, this, "Server user code changed: {0}", UserCode); + + var qrUrl = string.Format($"{Parent.Host}/api/rooms/{Parent.SystemUuid}/{RoomKey}/qr?x={new Random().Next()}"); + QrCodeUrl = qrUrl; + + Debug.Console(1, this, "Server user code changed: {0} - {1}", UserCode, qrUrl); + + OnUserCodeChanged(); + } + + protected void OnUserCodeChanged() + { + UserCodeChanged?.Invoke(this, new EventArgs()); + } + + protected void OnUserPromptedForCode() + { + UserPromptedForCode?.Invoke(this, new EventArgs()); + } + + protected void OnClientJoined() + { + ClientJoined?.Invoke(this, new EventArgs()); + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs new file mode 100644 index 00000000..a64d7e43 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs @@ -0,0 +1,977 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Room.MobileControl; +using PepperDash.Essentials.Room.Config; +using PepperDash.Essentials.Devices.Common.VideoCodec; +using PepperDash.Essentials.Devices.Common.AudioCodec; +using PepperDash.Essentials.Devices.Common.Cameras; + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Essentials.Devices.Common.Room; +using IShades = PepperDash.Essentials.Core.Shades.IShades; +using ShadeBase = PepperDash.Essentials.Devices.Common.Shades.ShadeBase; +using PepperDash.Essentials.Devices.Common.TouchPanel; +using Crestron.SimplSharp; +using Volume = PepperDash.Essentials.Room.MobileControl.Volume; +using PepperDash.Essentials.Core.CrestronIO; +using PepperDash.Essentials.Core.Lighting; +using PepperDash.Essentials.Core.Shades; +using PepperDash.Core.Logging; + + + +#if SERIES4 +using PepperDash.Essentials.AppServer; +#endif + +namespace PepperDash.Essentials +{ + public class MobileControlEssentialsRoomBridge : MobileControlBridgeBase + { + private List _touchPanelTokens = new List(); + public IEssentialsRoom Room { get; private set; } + + public string DefaultRoomKey { get; private set; } + /// + /// + /// + public override string RoomName + { + get { return Room.Name; } + } + + public override string RoomKey + { + get { return Room.Key; } + } + + public MobileControlEssentialsRoomBridge(IEssentialsRoom room) : + this($"mobileControlBridge-{room.Key}", room.Key, room) + { + Room = room; + } + + public MobileControlEssentialsRoomBridge(string key, string roomKey, IEssentialsRoom room) : base(key, $"/room/{room.Key}", room as Device) + { + DefaultRoomKey = roomKey; + + AddPreActivationAction(GetRoom); + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + // we add actions to the messaging system with a path, and a related action. Custom action + // content objects can be handled in the controller's LineReceived method - and perhaps other + // sub-controller parsing could be attached to these classes, so that the systemController + // doesn't need to know about everything. + + this.LogInformation("Registering Actions with AppServer"); + + AddAction("/promptForCode", (id, content) => OnUserPromptedForCode()); + AddAction("/clientJoined", (id, content) => OnClientJoined()); + + AddAction("/touchPanels", (id, content) => OnTouchPanelsUpdated(content)); + + AddAction($"/userApp", (id, content) => OnUserAppUpdated(content)); + + AddAction("/userCode", (id, content) => + { + var msg = content.ToObject(); + + SetUserCode(msg.UserCode, msg.QrChecksum ?? string.Empty); + }); + + + // Source Changes and room off + AddAction("/status", (id, content) => + { + SendFullStatusForClientId(id, Room); + }); + + if (Room is IRunRouteAction routeRoom) + AddAction("/source", (id, content) => + { + + var msg = content.ToObject(); + + this.LogVerbose("Received request to route to source: {sourceListKey} on list: {sourceList}", msg.SourceListItemKey, msg.SourceListKey); + + routeRoom.RunRouteAction(msg.SourceListItemKey, msg.SourceListKey); + }); + + if (Room is IRunDirectRouteAction directRouteRoom) + { + AddAction("/directRoute", (id, content) => + { + var msg = content.ToObject(); + + + this.LogVerbose("Running direct route from {sourceKey} to {destinationKey} with signal type {signalType}", msg.SourceKey, msg.DestinationKey, msg.SignalType); + + directRouteRoom.RunDirectRoute(msg.SourceKey, msg.DestinationKey, msg.SignalType); + }); + } + + + if (Room is IRunDefaultPresentRoute defaultRoom) + AddAction("/defaultsource", (id, content) => defaultRoom.RunDefaultPresentRoute()); + + if (Room is IHasCurrentVolumeControls volumeRoom) + { + volumeRoom.CurrentVolumeDeviceChange += Room_CurrentVolumeDeviceChange; + + if (volumeRoom.CurrentVolumeControls == null) return; + + AddAction("/volumes/master/level", (id, content) => + { + var msg = content.ToObject>(); + + + if (volumeRoom.CurrentVolumeControls is IBasicVolumeWithFeedback basicVolumeWithFeedback) + basicVolumeWithFeedback.SetVolume(msg.Value); + }); + + AddAction("/volumes/master/muteToggle", (id, content) => volumeRoom.CurrentVolumeControls.MuteToggle()); + + AddAction("/volumes/master/muteOn", (id, content) => + { + if (volumeRoom.CurrentVolumeControls is IBasicVolumeWithFeedback basicVolumeWithFeedback) + basicVolumeWithFeedback.MuteOn(); + }); + + AddAction("/volumes/master/muteOff", (id, content) => + { + if (volumeRoom.CurrentVolumeControls is IBasicVolumeWithFeedback basicVolumeWithFeedback) + basicVolumeWithFeedback.MuteOff(); + }); + + AddAction("/volumes/master/volumeUp", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => + { + if (volumeRoom.CurrentVolumeControls is IBasicVolumeWithFeedback basicVolumeWithFeedback) + { + basicVolumeWithFeedback.VolumeUp(b); + } + } + )); + + AddAction("/volumes/master/volumeDown", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => + { + if (volumeRoom.CurrentVolumeControls is IBasicVolumeWithFeedback basicVolumeWithFeedback) + { + basicVolumeWithFeedback.VolumeDown(b); + } + } + )); + + + // Registers for initial volume events, if possible + if (volumeRoom.CurrentVolumeControls is IBasicVolumeWithFeedback currentVolumeDevice) + { + this.LogVerbose("Registering for volume feedback events"); + + currentVolumeDevice.MuteFeedback.OutputChange += MuteFeedback_OutputChange; + currentVolumeDevice.VolumeLevelFeedback.OutputChange += VolumeLevelFeedback_OutputChange; + } + } + + if (Room is IHasCurrentSourceInfoChange sscRoom) + sscRoom.CurrentSourceChange += Room_CurrentSingleSourceChange; + + if (Room is IEssentialsHuddleVtc1Room vtcRoom) + { + if (vtcRoom.ScheduleSource != null) + { + var key = vtcRoom.Key + "-" + Key; + + if (!AppServerController.CheckForDeviceMessenger(key)) + { + var scheduleMessenger = new IHasScheduleAwarenessMessenger(key, vtcRoom.ScheduleSource, + $"/room/{vtcRoom.Key}"); + AppServerController.AddDeviceMessenger(scheduleMessenger); + } + } + + vtcRoom.InCallFeedback.OutputChange += InCallFeedback_OutputChange; + } + + if (Room is IPrivacy privacyRoom) + { + AddAction("/volumes/master/privacyMuteToggle", (id, content) => privacyRoom.PrivacyModeToggle()); + + privacyRoom.PrivacyModeIsOnFeedback.OutputChange += PrivacyModeIsOnFeedback_OutputChange; + } + + + if (Room is IRunDefaultCallRoute defCallRm) + { + AddAction("/activityVideo", (id, content) => defCallRm.RunDefaultCallRoute()); + } + + Room.OnFeedback.OutputChange += OnFeedback_OutputChange; + Room.IsCoolingDownFeedback.OutputChange += IsCoolingDownFeedback_OutputChange; + Room.IsWarmingUpFeedback.OutputChange += IsWarmingUpFeedback_OutputChange; + + AddTechRoomActions(); + } + + private void OnTouchPanelsUpdated(JToken content) + { + var message = content.ToObject(); + + _touchPanelTokens = message.TouchPanels; + + UpdateTouchPanelAppUrls(message.UserAppUrl); + } + + private void UpdateTouchPanelAppUrls(string userAppUrl) + { + foreach (var tp in _touchPanelTokens) + { + var dev = DeviceManager.AllDevices.OfType().FirstOrDefault((tpc) => tpc.Key.Equals(tp.TouchpanelKey, StringComparison.InvariantCultureIgnoreCase)); + + if (dev == null) + { + continue; + } + + //UpdateAppUrl($"{userAppUrl}?token={tp.Token}"); + + dev.SetAppUrl($"{userAppUrl}?token={tp.Token}"); + } + } + + private void OnUserAppUpdated(JToken content) + { + var message = content.ToObject(); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Updating User App URL to {userAppUrl}. Full Message: {@message}", this, message.UserAppUrl, content); + + UpdateTouchPanelAppUrls(message.UserAppUrl); + } + + private void InCallFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + var state = new RoomStateMessage + { + IsInCall = e.BoolValue + }; + PostStatusMessage(state); + } + + private void GetRoom() + { + if (Room != null) + { + this.LogInformation("Room with key {key} already linked.", DefaultRoomKey); + return; + } + + + if (!(DeviceManager.GetDeviceForKey(DefaultRoomKey) is IEssentialsRoom tempRoom)) + { + this.LogInformation("Room with key {key} not found or is not an Essentials Room", DefaultRoomKey); + return; + } + + Room = tempRoom; + } + + protected override void UserCodeChange() + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Server user code changed: {userCode}", this, UserCode); + + var qrUrl = string.Format("{0}/rooms/{1}/{3}/qr?x={2}", Parent?.Host, Parent?.SystemUuid, new Random().Next(), DefaultRoomKey); + + QrCodeUrl = qrUrl; + + this.LogDebug("Server user code changed: {userCode} - {qrUrl}", UserCode, qrUrl); + + OnUserCodeChanged(); + } + + /* /// + /// Override of base: calls base to add parent and then registers actions and events. + /// + /// + public override void AddParent(MobileControlSystemController parent) + { + base.AddParent(parent); + + }*/ + + private void AddTechRoomActions() + { + if (!(Room is IEssentialsTechRoom techRoom)) + { + return; + } + + AddAction("/roomPowerOn", (id, content) => techRoom.RoomPowerOn()); + AddAction("/roomPowerOff", (id, content) => techRoom.RoomPowerOff()); + } + + private void PrivacyModeIsOnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + var state = new RoomStateMessage(); + + var volumes = new Dictionary + { + { "master", new Volume("master") + { + PrivacyMuted = e.BoolValue + } + } + }; + + state.Volumes = volumes; + + PostStatusMessage(state); + } + + /// + /// + /// + /// + /// + private void IsSharingFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + // sharing source + string shareText; + bool isSharing; + + if (Room is IHasCurrentSourceInfoChange srcInfoRoom && (Room is IHasVideoCodec vcRoom && (vcRoom.VideoCodec.SharingContentIsOnFeedback.BoolValue && srcInfoRoom.CurrentSourceInfo != null))) + { + shareText = srcInfoRoom.CurrentSourceInfo.PreferredName; + isSharing = true; + } + else + { + shareText = "None"; + isSharing = false; + } + + var state = new RoomStateMessage + { + Share = new ShareState + { + CurrentShareText = shareText, + IsSharing = isSharing + } + }; + + PostStatusMessage(state); + } + + /// + /// + /// + /// + /// + private void IsWarmingUpFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + var state = new + { + isWarmingUp = e.BoolValue + }; + + PostStatusMessage(JToken.FromObject(state)); + } + + /// + /// + /// + /// + /// + private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + var state = new + { + isCoolingDown = e.BoolValue + }; + PostStatusMessage(JToken.FromObject(state)); + } + + /// + /// + /// + /// + /// + private void OnFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + var state = new + { + isOn = e.BoolValue + }; + PostStatusMessage(JToken.FromObject(state)); + } + + private void Room_CurrentVolumeDeviceChange(object sender, VolumeDeviceChangeEventArgs e) + { + if (e.OldDev is IBasicVolumeWithFeedback) + { + var oldDev = e.OldDev as IBasicVolumeWithFeedback; + oldDev.MuteFeedback.OutputChange -= MuteFeedback_OutputChange; + oldDev.VolumeLevelFeedback.OutputChange -= VolumeLevelFeedback_OutputChange; + } + + if (e.NewDev is IBasicVolumeWithFeedback) + { + var newDev = e.NewDev as IBasicVolumeWithFeedback; + newDev.MuteFeedback.OutputChange += MuteFeedback_OutputChange; + newDev.VolumeLevelFeedback.OutputChange += VolumeLevelFeedback_OutputChange; + } + } + + /// + /// Event handler for mute changes + /// + private void MuteFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + var state = new RoomStateMessage(); + + var volumes = new Dictionary + { + { "master", new Volume("master", e.BoolValue) } + }; + + state.Volumes = volumes; + + PostStatusMessage(state); + } + + /// + /// Handles Volume changes on room + /// + private void VolumeLevelFeedback_OutputChange(object sender, FeedbackEventArgs e) + { + + var state = new + { + volumes = new Dictionary + { + { "master", new Volume("master", e.IntValue) } + } + }; + PostStatusMessage(JToken.FromObject(state)); + } + + + private void Room_CurrentSingleSourceChange(SourceListItem info, ChangeType type) + { + /* Example message + * { +   "type":"/room/status", +   "content": { +     "selectedSourceKey": "off", +   } + } + */ + + } + + /// + /// Sends the full status of the room to the server + /// + /// + private void SendFullStatusForClientId(string id, IEssentialsRoom room) + { + //Parent.SendMessageObject(GetFullStatus(room)); + var message = GetFullStatusForClientId(room); + + if (message == null) + { + return; + } + PostStatusMessage(message, id); + } + + + /// + /// Gets full room status + /// + /// The room to get status of + /// The status response message + private RoomStateMessage GetFullStatusForClientId(IEssentialsRoom room) + { + try + { + this.LogVerbose("GetFullStatus"); + + var sourceKey = room is IHasCurrentSourceInfoChange ? (room as IHasCurrentSourceInfoChange).CurrentSourceInfoKey : null; + + var volumes = new Dictionary(); + if (room is IHasCurrentVolumeControls rmVc) + { + if (rmVc.CurrentVolumeControls is IBasicVolumeWithFeedback vc) + { + var volume = new Volume("master", vc.VolumeLevelFeedback.UShortValue, vc.MuteFeedback.BoolValue, "Volume", true, ""); + if (room is IPrivacy privacyRoom) + { + volume.HasPrivacyMute = true; + volume.PrivacyMuted = privacyRoom.PrivacyModeIsOnFeedback.BoolValue; + } + + volumes.Add("master", volume); + } + } + + var state = new RoomStateMessage + { + Configuration = GetRoomConfiguration(room), + ActivityMode = 1, + IsOn = room.OnFeedback.BoolValue, + SelectedSourceKey = sourceKey, + Volumes = volumes, + IsWarmingUp = room.IsWarmingUpFeedback.BoolValue, + IsCoolingDown = room.IsCoolingDownFeedback.BoolValue + }; + + if (room is IEssentialsHuddleVtc1Room vtcRoom) + { + state.IsInCall = vtcRoom.InCallFeedback.BoolValue; + } + + return state; + } catch (Exception ex) + { + Debug.LogMessage(ex, "Error getting full status", this); + return null; + } + } + + /// + /// Determines the configuration of the room and the details about the devices associated with the room + /// + /// + private RoomConfiguration GetRoomConfiguration(IEssentialsRoom room) + { + try + { + var configuration = new RoomConfiguration + { + //ShutdownPromptSeconds = room.ShutdownPromptSeconds, + TouchpanelKeys = DeviceManager.AllDevices. + OfType() + .Where((tp) => tp.DefaultRoomKey.Equals(room.Key, StringComparison.InvariantCultureIgnoreCase)) + .Select(tp => tp.Key).ToList() + }; + + try + { + var zrcTp = DeviceManager.AllDevices.OfType().SingleOrDefault((tp) => tp.ZoomRoomController); + + configuration.ZoomRoomControllerKey = zrcTp != null ? zrcTp.Key : null; + } + catch + { + configuration.ZoomRoomControllerKey = room.Key; + } + + if (room is IHasCiscoNavigatorTouchpanel ciscoNavRoom) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Setting CiscoNavigatorKey to: {ciscoNavRoom.CiscoNavigatorTouchpanelKey}", this); + configuration.CiscoNavigatorKey = ciscoNavRoom.CiscoNavigatorTouchpanelKey; + } + + + + // find the room combiner for this room by checking if the room is in the list of rooms for the room combiner + var roomCombiner = DeviceManager.AllDevices.OfType().FirstOrDefault(); + + configuration.RoomCombinerKey = roomCombiner != null ? roomCombiner.Key : null; + + + if (room is IEssentialsRoomPropertiesConfig propertiesConfig) + { + configuration.HelpMessage = propertiesConfig.PropertiesConfig.HelpMessageForDisplay; + } + + if (room is IEssentialsHuddleSpaceRoom huddleRoom && !string.IsNullOrEmpty(huddleRoom.PropertiesConfig.HelpMessageForDisplay)) + { + this.LogVerbose("Getting huddle room config"); + configuration.HelpMessage = huddleRoom.PropertiesConfig.HelpMessageForDisplay; + configuration.UiBehavior = huddleRoom.PropertiesConfig.UiBehavior; + configuration.DefaultPresentationSourceKey = huddleRoom.PropertiesConfig.DefaultSourceItem; + + } + + if (room is IEssentialsHuddleVtc1Room vtc1Room && !string.IsNullOrEmpty(vtc1Room.PropertiesConfig.HelpMessageForDisplay)) + { + this.LogVerbose("Getting vtc room config"); + configuration.HelpMessage = vtc1Room.PropertiesConfig.HelpMessageForDisplay; + configuration.UiBehavior = vtc1Room.PropertiesConfig.UiBehavior; + configuration.DefaultPresentationSourceKey = vtc1Room.PropertiesConfig.DefaultSourceItem; + } + + if (room is IEssentialsTechRoom techRoom && !string.IsNullOrEmpty(techRoom.PropertiesConfig.HelpMessage)) + { + this.LogVerbose("Getting tech room config"); + configuration.HelpMessage = techRoom.PropertiesConfig.HelpMessage; + } + + if (room is IHasVideoCodec vcRoom) + { + if (vcRoom.VideoCodec != null) + { + this.LogVerbose("Getting codec config"); + var type = vcRoom.VideoCodec.GetType(); + + configuration.HasVideoConferencing = true; + configuration.VideoCodecKey = vcRoom.VideoCodec.Key; + configuration.VideoCodecIsZoomRoom = type.Name.Equals("ZoomRoom", StringComparison.InvariantCultureIgnoreCase); + } + }; + + if (room is IHasAudioCodec acRoom) + { + if (acRoom.AudioCodec != null) + { + this.LogVerbose("Getting audio codec config"); + configuration.HasAudioConferencing = true; + configuration.AudioCodecKey = acRoom.AudioCodec.Key; + } + } + + + if (room is IHasMatrixRouting matrixRoutingRoom) + { + this.LogVerbose("Getting matrix routing config"); + configuration.MatrixRoutingKey = matrixRoutingRoom.MatrixRoutingDeviceKey; + configuration.EndpointKeys = matrixRoutingRoom.EndpointKeys; + } + + if (room is IEnvironmentalControls envRoom) + { + this.LogVerbose("Getting environmental controls config. RoomHasEnvironmentalControls: {hasEnvironmentalControls}", envRoom.HasEnvironmentalControlDevices); + configuration.HasEnvironmentalControls = envRoom.HasEnvironmentalControlDevices; + + if (envRoom.HasEnvironmentalControlDevices) + { + this.LogVerbose("Room Has {count} Environmental Control Devices.", envRoom.EnvironmentalControlDevices.Count); + + foreach (var dev in envRoom.EnvironmentalControlDevices) + { + this.LogVerbose("Adding environmental device: {key}", dev.Key); + + eEnvironmentalDeviceTypes type = eEnvironmentalDeviceTypes.None; + + if (dev is ILightingScenes || dev is Devices.Common.Lighting.LightingBase) + { + type = eEnvironmentalDeviceTypes.Lighting; + } + else if (dev is ShadeBase || dev is IShadesOpenCloseStop || dev is IShadesOpenClosePreset) + { + type = eEnvironmentalDeviceTypes.Shade; + } + else if (dev is IShades) + { + type = eEnvironmentalDeviceTypes.ShadeController; + } + else if (dev is ISwitchedOutput) + { + type = eEnvironmentalDeviceTypes.Relay; + } + + this.LogVerbose("Environmental Device Type: {type}", type); + + var envDevice = new EnvironmentalDeviceConfiguration(dev.Key, type); + + configuration.EnvironmentalDevices.Add(envDevice); + } + } + else + { + this.LogVerbose("Room Has No Environmental Control Devices"); + } + } + + if (room is IHasDefaultDisplay defDisplayRoom) + { + this.LogVerbose("Getting default display config"); + configuration.DefaultDisplayKey = defDisplayRoom.DefaultDisplay.Key; + configuration.Destinations.Add(eSourceListItemDestinationTypes.defaultDisplay, defDisplayRoom.DefaultDisplay.Key); + } + + if (room is IHasMultipleDisplays multiDisplayRoom) + { + this.LogVerbose("Getting multiple display config"); + + if (multiDisplayRoom.Displays == null) + { + this.LogVerbose("Displays collection is null"); + } + else + { + this.LogVerbose("Displays collection exists"); + + configuration.Destinations = multiDisplayRoom.Displays.ToDictionary(kv => kv.Key, kv => kv.Value.Key); + } + } + + if (room is IHasAccessoryDevices accRoom) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Getting accessory devices config", this); + + if (accRoom.AccessoryDeviceKeys == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Accessory devices collection is null", this); + } + else + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Accessory devices collection exists", this); + + configuration.AccessoryDeviceKeys = accRoom.AccessoryDeviceKeys; + } + } + + var sourceList = ConfigReader.ConfigObject.GetSourceListForKey(room.SourceListKey); + if (sourceList != null) + { + this.LogVerbose("Getting source list config"); + configuration.SourceList = sourceList; + configuration.HasRoutingControls = true; + + foreach (var source in sourceList) + { + if (source.Value.SourceDevice is Devices.Common.IRSetTopBoxBase) + { + configuration.HasSetTopBoxControls = true; + continue; + } + else if (source.Value.SourceDevice is CameraBase) + { + configuration.HasCameraControls = true; + continue; + } + } + } + + var destinationList = ConfigReader.ConfigObject.GetDestinationListForKey(room.DestinationListKey); + + if (destinationList != null) + { + configuration.DestinationList = destinationList; + } + + var audioControlPointList = ConfigReader.ConfigObject.GetAudioControlPointListForKey(room.AudioControlPointListKey); + + if (audioControlPointList != null) + { + configuration.AudioControlPointList = audioControlPointList; + } + + var cameraList = ConfigReader.ConfigObject.GetCameraListForKey(room.CameraListKey); + + if (cameraList != null) + { + configuration.CameraList = cameraList; + } + + return configuration; + + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Exception getting room configuration"); + return new RoomConfiguration(); + } + } + + } + + public class RoomStateMessage : DeviceStateMessageBase + { + [JsonProperty("configuration", NullValueHandling = NullValueHandling.Ignore)] + public RoomConfiguration Configuration { get; set; } + + [JsonProperty("activityMode", NullValueHandling = NullValueHandling.Ignore)] + public int? ActivityMode { get; set; } + [JsonProperty("advancedSharingActive", NullValueHandling = NullValueHandling.Ignore)] + public bool? AdvancedSharingActive { get; set; } + [JsonProperty("isOn", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsOn { get; set; } + [JsonProperty("isWarmingUp", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsWarmingUp { get; set; } + [JsonProperty("isCoolingDown", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsCoolingDown { get; set; } + [JsonProperty("selectedSourceKey", NullValueHandling = NullValueHandling.Ignore)] + public string SelectedSourceKey { get; set; } + [JsonProperty("share", NullValueHandling = NullValueHandling.Ignore)] + public ShareState Share { get; set; } + + [JsonProperty("volumes", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary Volumes { get; set; } + + [JsonProperty("isInCall", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsInCall { get; set; } + } + + public class ShareState + { + [JsonProperty("currentShareText", NullValueHandling = NullValueHandling.Ignore)] + public string CurrentShareText { get; set; } + [JsonProperty("enabled", NullValueHandling = NullValueHandling.Ignore)] + public bool? Enabled { get; set; } + [JsonProperty("isSharing", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsSharing { get; set; } + } + + /// + /// Represents the capabilities of the room and the associated device info + /// + public class RoomConfiguration + { + //[JsonProperty("shutdownPromptSeconds", NullValueHandling = NullValueHandling.Ignore)] + //public int? ShutdownPromptSeconds { get; set; } + + [JsonProperty("hasVideoConferencing", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasVideoConferencing { get; set; } + [JsonProperty("videoCodecIsZoomRoom", NullValueHandling = NullValueHandling.Ignore)] + public bool? VideoCodecIsZoomRoom { get; set; } + [JsonProperty("hasAudioConferencing", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasAudioConferencing { get; set; } + [JsonProperty("hasEnvironmentalControls", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasEnvironmentalControls { get; set; } + [JsonProperty("hasCameraControls", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasCameraControls { get; set; } + [JsonProperty("hasSetTopBoxControls", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasSetTopBoxControls { get; set; } + [JsonProperty("hasRoutingControls", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasRoutingControls { get; set; } + + [JsonProperty("touchpanelKeys", NullValueHandling = NullValueHandling.Ignore)] + public List TouchpanelKeys { get; set; } + + [JsonProperty("zoomRoomControllerKey", NullValueHandling = NullValueHandling.Ignore)] + public string ZoomRoomControllerKey { get; set; } + + [JsonProperty("ciscoNavigatorKey", NullValueHandling = NullValueHandling.Ignore)] + public string CiscoNavigatorKey { get; set; } + + + [JsonProperty("videoCodecKey", NullValueHandling = NullValueHandling.Ignore)] + public string VideoCodecKey { get; set; } + [JsonProperty("audioCodecKey", NullValueHandling = NullValueHandling.Ignore)] + public string AudioCodecKey { get; set; } + [JsonProperty("matrixRoutingKey", NullValueHandling = NullValueHandling.Ignore)] + public string MatrixRoutingKey { get; set; } + [JsonProperty("endpointKeys", NullValueHandling = NullValueHandling.Ignore)] + public List EndpointKeys { get; set; } + + [JsonProperty("accessoryDeviceKeys", NullValueHandling = NullValueHandling.Ignore)] + public List AccessoryDeviceKeys { get; set; } + + [JsonProperty("defaultDisplayKey", NullValueHandling = NullValueHandling.Ignore)] + public string DefaultDisplayKey { get; set; } + [JsonProperty("destinations", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary Destinations { get; set; } + [JsonProperty("environmentalDevices", NullValueHandling = NullValueHandling.Ignore)] + public List EnvironmentalDevices { get; set; } + [JsonProperty("sourceList", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary SourceList { get; set; } + + [JsonProperty("destinationList", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary DestinationList { get; set;} + + [JsonProperty("audioControlPointList", NullValueHandling = NullValueHandling.Ignore)] + public AudioControlPointListItem AudioControlPointList { get; set; } + + [JsonProperty("cameraList", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary CameraList { get; set; } + + [JsonProperty("defaultPresentationSourceKey", NullValueHandling = NullValueHandling.Ignore)] + public string DefaultPresentationSourceKey { get; set; } + + + [JsonProperty("helpMessage", NullValueHandling = NullValueHandling.Ignore)] + public string HelpMessage { get; set; } + + [JsonProperty("techPassword", NullValueHandling = NullValueHandling.Ignore)] + public string TechPassword { get; set; } + + [JsonProperty("uiBehavior", NullValueHandling = NullValueHandling.Ignore)] + public EssentialsRoomUiBehaviorConfig UiBehavior { get; set; } + + [JsonProperty("supportsAdvancedSharing", NullValueHandling = NullValueHandling.Ignore)] + public bool? SupportsAdvancedSharing { get; set; } + [JsonProperty("userCanChangeShareMode", NullValueHandling = NullValueHandling.Ignore)] + public bool? UserCanChangeShareMode { get; set; } + + [JsonProperty("roomCombinerKey", NullValueHandling = NullValueHandling.Ignore)] + public string RoomCombinerKey { get; set; } + + public RoomConfiguration() + { + Destinations = new Dictionary(); + EnvironmentalDevices = new List(); + SourceList = new Dictionary(); + TouchpanelKeys = new List(); + } + } + + public class EnvironmentalDeviceConfiguration + { + [JsonProperty("deviceKey", NullValueHandling = NullValueHandling.Ignore)] + public string DeviceKey { get; private set; } + + [JsonConverter(typeof(StringEnumConverter))] + [JsonProperty("deviceType", NullValueHandling = NullValueHandling.Ignore)] + public eEnvironmentalDeviceTypes DeviceType { get; private set; } + + public EnvironmentalDeviceConfiguration(string key, eEnvironmentalDeviceTypes type) + { + DeviceKey = key; + DeviceType = type; + } + } + + public enum eEnvironmentalDeviceTypes + { + None, + Lighting, + Shade, + ShadeController, + Relay, + } + + public class ApiTouchPanelToken + { + [JsonProperty("touchPanels", NullValueHandling = NullValueHandling.Ignore)] + public List TouchPanels { get; set; } = new List(); + + [JsonProperty("userAppUrl", NullValueHandling = NullValueHandling.Ignore)] + public string UserAppUrl { get; set; } = ""; + } + +#if SERIES3 + public class SourceSelectMessageContent + { + public string SourceListItem { get; set; } + public string SourceListKey { get; set; } + } + + public class DirectRoute + { + public string SourceKey { get; set; } + public string DestinationKey { get; set; } + } + + /// + /// + /// + /// + public delegate void PressAndHoldAction(bool b); +#endif +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs new file mode 100644 index 00000000..a617200c --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs @@ -0,0 +1,1128 @@ +using Crestron.SimplSharp; +using Crestron.SimplSharp.Reflection; +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.EthernetCommunication; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.AppServer; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Codec; +using PepperDash.Essentials.Room.Config; +using System; +using System.Collections.Generic; + + +namespace PepperDash.Essentials.Room.MobileControl +{ + // ReSharper disable once InconsistentNaming + public class MobileControlSIMPLRoomBridge : MobileControlBridgeBase, IDelayedConfiguration + { + private const int SupportedDisplayCount = 10; + + /// + /// Fires when config is ready to go + /// + public event EventHandler ConfigurationIsReady; + + public ThreeSeriesTcpIpEthernetIntersystemCommunications Eisc { get; private set; } + + public MobileControlSIMPLRoomJoinMap JoinMap { get; private set; } + + public Dictionary DeviceMessengers { get; private set; } + + + /// + /// + /// + public bool ConfigIsLoaded { get; private set; } + + public override string RoomName + { + get + { + var name = Eisc.StringOutput[JoinMap.ConfigRoomName.JoinNumber].StringValue; + return string.IsNullOrEmpty(name) ? "Not Loaded" : name; + } + } + + public override string RoomKey + { + get { return "room1"; } + } + + private readonly MobileControlSimplDeviceBridge _sourceBridge; + + private SIMPLAtcMessenger _atcMessenger; + private SIMPLVtcMessenger _vtcMessenger; + private SimplDirectRouteMessenger _directRouteMessenger; + + private const string _syntheticDeviceKey = "syntheticDevice"; + + /// + /// + /// + /// + /// + /// + public MobileControlSIMPLRoomBridge(string key, string name, uint ipId) + : base(key, "") + { + Eisc = new ThreeSeriesTcpIpEthernetIntersystemCommunications(ipId, "127.0.0.2", Global.ControlSystem); + var reg = Eisc.Register(); + if (reg != eDeviceRegistrationUnRegistrationResponse.Success) + Debug.Console(0, this, "Cannot connect EISC at IPID {0}: \r{1}", ipId, reg); + + JoinMap = new MobileControlSIMPLRoomJoinMap(1); + + _sourceBridge = new MobileControlSimplDeviceBridge(key + "-sourceBridge", "SIMPL source bridge", Eisc); + DeviceManager.AddDevice(_sourceBridge); + + CrestronConsole.AddNewConsoleCommand((s) => JoinMap.PrintJoinMapInfo(), "printmobilejoinmap", "Prints the MobileControlSIMPLRoomBridge JoinMap", ConsoleAccessLevelEnum.AccessOperator); + + AddPostActivationAction(() => + { + // Inform the SIMPL program that config can be sent + Eisc.BooleanInput[JoinMap.ReadyForConfig.JoinNumber].BoolValue = true; + + Eisc.SigChange += EISC_SigChange; + Eisc.OnlineStatusChange += (o, a) => + { + if (!a.DeviceOnLine) + { + return; + } + + Debug.Console(1, this, "SIMPL EISC online={0}. Config is ready={1}. Use Essentials Config={2}", + a.DeviceOnLine, Eisc.BooleanOutput[JoinMap.ConfigIsReady.JoinNumber].BoolValue, + Eisc.BooleanOutput[JoinMap.ConfigIsLocal.JoinNumber].BoolValue); + + if (Eisc.BooleanOutput[JoinMap.ConfigIsReady.JoinNumber].BoolValue) + LoadConfigValues(); + + if (Eisc.BooleanOutput[JoinMap.ConfigIsLocal.JoinNumber].BoolValue) + UseEssentialsConfig(); + }; + // load config if it's already there + if (Eisc.BooleanOutput[JoinMap.ConfigIsReady.JoinNumber].BoolValue) + { + LoadConfigValues(); + } + + if (Eisc.BooleanOutput[JoinMap.ConfigIsLocal.JoinNumber].BoolValue) + { + UseEssentialsConfig(); + } + }); + } + + + /// + /// Finish wiring up everything after all devices are created. The base class will hunt down the related + /// parent controller and link them up. + /// + /// + public override bool CustomActivate() + { + Debug.Console(0, this, "Final activation. Setting up actions and feedbacks"); + //SetupFunctions(); + //SetupFeedbacks(); + + var atcKey = string.Format("atc-{0}-{1}", Key, Key); + _atcMessenger = new SIMPLAtcMessenger(atcKey, Eisc, "/device/audioCodec"); + _atcMessenger.RegisterWithAppServer(Parent); + + var vtcKey = string.Format("atc-{0}-{1}", Key, Key); + _vtcMessenger = new SIMPLVtcMessenger(vtcKey, Eisc, "/device/videoCodec"); + _vtcMessenger.RegisterWithAppServer(Parent); + + var drKey = string.Format("directRoute-{0}-{1}", Key, Key); + _directRouteMessenger = new SimplDirectRouteMessenger(drKey, Eisc, "/routing"); + _directRouteMessenger.RegisterWithAppServer(Parent); + + CrestronConsole.AddNewConsoleCommand(s => + { + JoinMap.PrintJoinMapInfo(); + + _atcMessenger.JoinMap.PrintJoinMapInfo(); + + _vtcMessenger.JoinMap.PrintJoinMapInfo(); + + _directRouteMessenger.JoinMap.PrintJoinMapInfo(); + + // TODO: Update Source Bridge to use new JoinMap scheme + //_sourceBridge.JoinMap.PrintJoinMapInfo(); + }, "printmobilebridge", "Prints MC-SIMPL bridge EISC data", ConsoleAccessLevelEnum.AccessOperator); + + return base.CustomActivate(); + } + + private void UseEssentialsConfig() + { + ConfigIsLoaded = false; + + SetupDeviceMessengers(); + + Debug.Console(0, this, "******* ESSENTIALS CONFIG: \r{0}", + JsonConvert.SerializeObject(ConfigReader.ConfigObject, Formatting.Indented)); + + ConfigurationIsReady?.Invoke(this, new EventArgs()); + + ConfigIsLoaded = true; + } + +#if SERIES4 + protected override void RegisterActions() +#else + protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) +#endif + { + SetupFunctions(); + SetupFeedbacks(); + } + + /// + /// Setup the actions to take place on various incoming API calls + /// + private void SetupFunctions() + { + AddAction(@"/promptForCode", + (id, content) => Eisc.PulseBool(JoinMap.PromptForCode.JoinNumber)); + AddAction(@"/clientJoined", (id, content) => Eisc.PulseBool(JoinMap.ClientJoined.JoinNumber)); + + AddAction(@"/status", (id, content) => SendFullStatus()); + + AddAction(@"/source", (id, content) => + { + var msg = content.ToObject(); + + Eisc.SetString(JoinMap.CurrentSourceKey.JoinNumber, msg.SourceListItemKey); + Eisc.PulseBool(JoinMap.SourceHasChanged.JoinNumber); + }); + + AddAction(@"/defaultsource", (id, content) => + Eisc.PulseBool(JoinMap.ActivityShare.JoinNumber)); + AddAction(@"/activityPhone", (id, content) => + Eisc.PulseBool(JoinMap.ActivityPhoneCall.JoinNumber)); + AddAction(@"/activityVideo", (id, content) => + Eisc.PulseBool(JoinMap.ActivityVideoCall.JoinNumber)); + + AddAction(@"/volumes/master/level", (id, content) => + { + var value = content["value"].Value(); + + Eisc.SetUshort(JoinMap.MasterVolume.JoinNumber, value); + }); + + AddAction(@"/volumes/master/muteToggle", (id, content) => + Eisc.PulseBool(JoinMap.MasterVolume.JoinNumber)); + AddAction(@"/volumes/master/privacyMuteToggle", (id, content) => + Eisc.PulseBool(JoinMap.PrivacyMute.JoinNumber)); + + + // /xyzxyz/volumes/master/muteToggle ---> BoolInput[1] + + var volumeStart = JoinMap.VolumeJoinStart.JoinNumber; + var volumeEnd = JoinMap.VolumeJoinStart.JoinNumber + JoinMap.VolumeJoinStart.JoinSpan; + + for (uint i = volumeStart; i <= volumeEnd; i++) + { + var index = i; + AddAction(string.Format(@"/volumes/level-{0}/level", index), (id, content) => + { + var value = content["value"].Value(); + Eisc.SetUshort(index, value); + }); + + AddAction(string.Format(@"/volumes/level-{0}/muteToggle", index), (id, content) => + Eisc.PulseBool(index)); + } + + AddAction(@"/shutdownStart", (id, content) => + Eisc.PulseBool(JoinMap.ShutdownStart.JoinNumber)); + AddAction(@"/shutdownEnd", (id, content) => + Eisc.PulseBool(JoinMap.ShutdownEnd.JoinNumber)); + AddAction(@"/shutdownCancel", (id, content) => + Eisc.PulseBool(JoinMap.ShutdownCancel.JoinNumber)); + } + + + /// + /// + /// + /// + private void SetupSourceFunctions(string devKey) + { + var sourceJoinMap = new SourceDeviceMapDictionary(); + + var prefix = string.Format("/device/{0}/", devKey); + + foreach (var item in sourceJoinMap) + { + var join = item.Value; + AddAction(string.Format("{0}{1}", prefix, item.Key), (id, content) => + { + HandlePressAndHoldEisc(content, b => Eisc.SetBool(join, b)); + }); + } + } + + private void HandlePressAndHoldEisc(JToken content, Action action) + { + var state = content.ToObject>(); + + var timerHandler = PressAndHoldHandler.GetPressAndHoldHandler(state.Value); + if (timerHandler == null) + { + return; + } + + timerHandler(state.Value, action); + + action(state.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase)); + } + + + /// + /// Links feedbacks to whatever is gonna happen! + /// + private void SetupFeedbacks() + { + // Power + Eisc.SetBoolSigAction(JoinMap.RoomIsOn.JoinNumber, b => + PostStatus(new + { + isOn = b + })); + + // Source change things + Eisc.SetSigTrueAction(JoinMap.SourceHasChanged.JoinNumber, () => + PostStatus(new + { + selectedSourceKey = Eisc.StringOutput[JoinMap.CurrentSourceKey.JoinNumber].StringValue + })); + + // Volume things + Eisc.SetUShortSigAction(JoinMap.MasterVolume.JoinNumber, u => + PostStatus(new + { + volumes = new + { + master = new + { + level = u + } + } + })); + + // map MasterVolumeIsMuted join -> status/volumes/master/muted + // + + Eisc.SetBoolSigAction(JoinMap.MasterVolume.JoinNumber, b => + PostStatus(new + { + volumes = new + { + master = new + { + muted = b + } + } + })); + Eisc.SetBoolSigAction(JoinMap.PrivacyMute.JoinNumber, b => + PostStatus(new + { + volumes = new + { + master = new + { + privacyMuted = b + } + } + })); + + var volumeStart = JoinMap.VolumeJoinStart.JoinNumber; + var volumeEnd = JoinMap.VolumeJoinStart.JoinNumber + JoinMap.VolumeJoinStart.JoinSpan; + + for (uint i = volumeStart; i <= volumeEnd; i++) + { + var index = i; // local scope for lambdas + Eisc.SetUShortSigAction(index, u => // start at join 2 + { + // need a dict in order to create the level-n property on auxFaders + var dict = new Dictionary { { "level-" + index, new { level = u } } }; + PostStatus(new + { + volumes = new + { + auxFaders = dict, + } + }); + }); + Eisc.SetBoolSigAction(index, b => + { + // need a dict in order to create the level-n property on auxFaders + var dict = new Dictionary { { "level-" + index, new { muted = b } } }; + PostStatus(new + { + volumes = new + { + auxFaders = dict, + } + }); + }); + } + + Eisc.SetUShortSigAction(JoinMap.NumberOfAuxFaders.JoinNumber, u => + PostStatus(new + { + volumes = new + { + numberOfAuxFaders = u, + } + })); + + // shutdown things + Eisc.SetSigTrueAction(JoinMap.ShutdownCancel.JoinNumber, () => + PostMessage("/shutdown/", new + { + state = "wasCancelled" + })); + Eisc.SetSigTrueAction(JoinMap.ShutdownEnd.JoinNumber, () => + PostMessage("/shutdown/", new + { + state = "hasFinished" + })); + Eisc.SetSigTrueAction(JoinMap.ShutdownStart.JoinNumber, () => + PostMessage("/shutdown/", new + { + state = "hasStarted", + duration = Eisc.UShortOutput[JoinMap.ShutdownPromptDuration.JoinNumber].UShortValue + })); + + // Config things + Eisc.SetSigTrueAction(JoinMap.ConfigIsReady.JoinNumber, LoadConfigValues); + + // Activity modes + Eisc.SetSigTrueAction(JoinMap.ActivityShare.JoinNumber, () => UpdateActivity(1)); + Eisc.SetSigTrueAction(JoinMap.ActivityPhoneCall.JoinNumber, () => UpdateActivity(2)); + Eisc.SetSigTrueAction(JoinMap.ActivityVideoCall.JoinNumber, () => UpdateActivity(3)); + + AppServerController.ApiOnlineAndAuthorized.LinkInputSig(Eisc.BooleanInput[JoinMap.ApiOnlineAndAuthorized.JoinNumber]); + } + + + /// + /// Updates activity states + /// + private void UpdateActivity(int mode) + { + PostStatus(new + { + activityMode = mode, + }); + } + + /// + /// Synthesizes a source device config from the SIMPL config join data + /// + /// + /// + /// + /// + private DeviceConfig GetSyntheticSourceDevice(SourceListItem sli, string type, uint i) + { + var groupMap = GetSourceGroupDictionary(); + var key = sli.SourceKey; + var name = sli.Name; + + // If not, synthesize the device config + var group = "genericsource"; + if (groupMap.ContainsKey(type)) + { + group = groupMap[type]; + } + + // add dev to devices list + var devConf = new DeviceConfig + { + Group = group, + Key = key, + Name = name, + Type = type, + Properties = new JObject(new JProperty(_syntheticDeviceKey, true)), + }; + + if (group.ToLower().StartsWith("settopbox")) // Add others here as needed + { + SetupSourceFunctions(key); + } + + if (group.ToLower().Equals("simplmessenger")) + { + if (type.ToLower().Equals("simplcameramessenger")) + { + var props = new SimplMessengerPropertiesConfig + { + DeviceKey = key, + JoinMapKey = "" + }; + var joinStart = 1000 + (i * 100) + 1; // 1001, 1101, 1201, 1301... etc. + props.JoinStart = joinStart; + devConf.Properties = JToken.FromObject(props); + } + } + + return devConf; + } + + /// + /// Reads in config values when the Simpl program is ready + /// + private void LoadConfigValues() + { + Debug.Console(1, this, "Loading configuration from SIMPL EISC bridge"); + ConfigIsLoaded = false; + + var co = ConfigReader.ConfigObject; + + if (!string.IsNullOrEmpty(Eisc.StringOutput[JoinMap.PortalSystemUrl.JoinNumber].StringValue)) + { + ConfigReader.ConfigObject.SystemUrl = Eisc.StringOutput[JoinMap.PortalSystemUrl.JoinNumber].StringValue; + } + + co.Info.RuntimeInfo.AppName = Assembly.GetExecutingAssembly().GetName().Name; + var version = Assembly.GetExecutingAssembly().GetName().Version; + co.Info.RuntimeInfo.AssemblyVersion = string.Format("{0}.{1}.{2}", version.Major, version.Minor, + version.Build); + + //Room + //if (co.Rooms == null) + // always start fresh in case simpl changed + co.Rooms = new List(); + var rm = new DeviceConfig(); + if (co.Rooms.Count == 0) + { + Debug.Console(0, this, "Adding room to config"); + co.Rooms.Add(rm); + } + else + { + Debug.Console(0, this, "Replacing Room[0] in config"); + co.Rooms[0] = rm; + } + rm.Name = Eisc.StringOutput[JoinMap.ConfigRoomName.JoinNumber].StringValue; + rm.Key = "room1"; + rm.Type = "SIMPL01"; + + var rmProps = rm.Properties == null + ? new SimplRoomPropertiesConfig() + : JsonConvert.DeserializeObject(rm.Properties.ToString()); + + rmProps.Help = new EssentialsHelpPropertiesConfig + { + CallButtonText = Eisc.StringOutput[JoinMap.ConfigHelpNumber.JoinNumber].StringValue, + Message = Eisc.StringOutput[JoinMap.ConfigHelpMessage.JoinNumber].StringValue + }; + + rmProps.Environment = new EssentialsEnvironmentPropertiesConfig(); // enabled defaults to false + + rmProps.RoomPhoneNumber = Eisc.StringOutput[JoinMap.ConfigRoomPhoneNumber.JoinNumber].StringValue; + rmProps.RoomURI = Eisc.StringOutput[JoinMap.ConfigRoomUri.JoinNumber].StringValue; + rmProps.SpeedDials = new List(); + + // This MAY need a check + if (Eisc.BooleanOutput[JoinMap.ActivityPhoneCallEnable.JoinNumber].BoolValue) + { + rmProps.AudioCodecKey = "audioCodec"; + } + + if (Eisc.BooleanOutput[JoinMap.ActivityVideoCallEnable.JoinNumber].BoolValue) + { + rmProps.VideoCodecKey = "videoCodec"; + } + + // volume control names + + //// use Volumes object or? + //rmProps.VolumeSliderNames = new List(); + //for(uint i = 701; i <= 700 + volCount; i++) + //{ + // rmProps.VolumeSliderNames.Add(EISC.StringInput[i].StringValue); + //} + + // There should be Mobile Control devices in here, I think... + if (co.Devices == null) + co.Devices = new List(); + + // clear out previous SIMPL devices + co.Devices.RemoveAll(d => + d.Key.StartsWith("source-", StringComparison.OrdinalIgnoreCase) + || d.Key.Equals("audioCodec", StringComparison.OrdinalIgnoreCase) + || d.Key.Equals("videoCodec", StringComparison.OrdinalIgnoreCase) + || d.Key.StartsWith("destination-", StringComparison.OrdinalIgnoreCase)); + + rmProps.SourceListKey = "default"; + rm.Properties = JToken.FromObject(rmProps); + + // Source list! This might be brutal!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + co.SourceLists = new Dictionary>(); + var newSl = new Dictionary(); + // add "none" source if VTC present + + if (!string.IsNullOrEmpty(rmProps.VideoCodecKey)) + { + var codecOsd = new SourceListItem + { + Name = "None", + IncludeInSourceList = true, + Order = 1, + Type = eSourceListItemType.Route, + SourceKey = "" + }; + newSl.Add("Source-None", codecOsd); + } + // add sources... + var useSourceEnabled = Eisc.BooleanOutput[JoinMap.UseSourceEnabled.JoinNumber].BoolValue; + for (uint i = 0; i <= 19; i++) + { + var name = Eisc.StringOutput[JoinMap.SourceNameJoinStart.JoinNumber + i].StringValue; + + if (!Eisc.BooleanOutput[JoinMap.UseSourceEnabled.JoinNumber].BoolValue && string.IsNullOrEmpty(name)) + { + Debug.Console(1, "Source at join {0} does not have a name", JoinMap.SourceNameJoinStart.JoinNumber + i); + break; + } + + + var icon = Eisc.StringOutput[JoinMap.SourceIconJoinStart.JoinNumber + i].StringValue; + var key = Eisc.StringOutput[JoinMap.SourceKeyJoinStart.JoinNumber + i].StringValue; + var type = Eisc.StringOutput[JoinMap.SourceTypeJoinStart.JoinNumber + i].StringValue; + var disableShare = Eisc.BooleanOutput[JoinMap.SourceShareDisableJoinStart.JoinNumber + i].BoolValue; + var sourceEnabled = Eisc.BooleanOutput[JoinMap.SourceIsEnabledJoinStart.JoinNumber + i].BoolValue; + var controllable = Eisc.BooleanOutput[JoinMap.SourceIsControllableJoinStart.JoinNumber + i].BoolValue; + var audioSource = Eisc.BooleanOutput[JoinMap.SourceIsAudioSourceJoinStart.JoinNumber + i].BoolValue; + + Debug.Console(0, this, "Adding source {0} '{1}'", key, name); + + var sourceKey = Eisc.StringOutput[JoinMap.SourceControlDeviceKeyJoinStart.JoinNumber + i].StringValue; + + var newSli = new SourceListItem + { + Icon = icon, + Name = name, + Order = (int)i + 10, + SourceKey = string.IsNullOrEmpty(sourceKey) ? key : sourceKey, // Use the value from the join if defined + Type = eSourceListItemType.Route, + DisableCodecSharing = disableShare, + IncludeInSourceList = !useSourceEnabled || sourceEnabled, + IsControllable = controllable, + IsAudioSource = audioSource + }; + newSl.Add(key, newSli); + + var existingSourceDevice = co.GetDeviceForKey(newSli.SourceKey); + + var syntheticDevice = GetSyntheticSourceDevice(newSli, type, i); + + // Look to see if this is a device that already exists in Essentials and get it + if (existingSourceDevice != null) + { + Debug.Console(0, this, "Found device with key: {0} in Essentials.", key); + + if (existingSourceDevice.Properties.Value(_syntheticDeviceKey)) + { + Debug.Console(0, this, "Updating previous device config with new values"); + existingSourceDevice = syntheticDevice; + } + else + { + Debug.Console(0, this, "Using existing Essentials device (non synthetic)"); + } + } + else + { + co.Devices.Add(syntheticDevice); + } + } + + co.SourceLists.Add("default", newSl); + + if (Eisc.BooleanOutput[JoinMap.SupportsAdvancedSharing.JoinNumber].BoolValue) + { + if (co.DestinationLists == null) + { + co.DestinationLists = new Dictionary>(); + } + + CreateDestinationList(co); + } + + // Build "audioCodec" config if we need + if (!string.IsNullOrEmpty(rmProps.AudioCodecKey)) + { + var acFavs = new List(); + for (uint i = 0; i < 4; i++) + { + if (!Eisc.GetBool(JoinMap.SpeedDialVisibleStartJoin.JoinNumber + i)) + { + break; + } + acFavs.Add(new CodecActiveCallItem + { + Name = Eisc.GetString(JoinMap.SpeedDialNameStartJoin.JoinNumber + i), + Number = Eisc.GetString(JoinMap.SpeedDialNumberStartJoin.JoinNumber + i), + Type = eCodecCallType.Audio + }); + } + + var acProps = new + { + favorites = acFavs + }; + + const string acStr = "audioCodec"; + var acConf = new DeviceConfig + { + Group = acStr, + Key = acStr, + Name = acStr, + Type = acStr, + Properties = JToken.FromObject(acProps) + }; + co.Devices.Add(acConf); + } + + // Build Video codec config + if (!string.IsNullOrEmpty(rmProps.VideoCodecKey)) + { + // No favorites, for now? + var favs = new List(); + + // cameras + var camsProps = new List(); + for (uint i = 0; i < 9; i++) + { + var name = Eisc.GetString(i + JoinMap.CameraNearNameStart.JoinNumber); + if (!string.IsNullOrEmpty(name)) + { + camsProps.Add(new + { + name, + selector = "camera" + (i + 1), + }); + } + } + var farName = Eisc.GetString(JoinMap.CameraFarName.JoinNumber); + if (!string.IsNullOrEmpty(farName)) + { + camsProps.Add(new + { + name = farName, + selector = "cameraFar", + }); + } + + var props = new + { + favorites = favs, + cameras = camsProps, + }; + const string str = "videoCodec"; + var conf = new DeviceConfig + { + Group = str, + Key = str, + Name = str, + Type = str, + Properties = JToken.FromObject(props) + }; + co.Devices.Add(conf); + } + + SetupDeviceMessengers(); + + Debug.Console(0, this, "******* CONFIG FROM SIMPL: \r{0}", + JsonConvert.SerializeObject(ConfigReader.ConfigObject, Formatting.Indented)); + + ConfigurationIsReady?.Invoke(this, new EventArgs()); + + ConfigIsLoaded = true; + } + + private DeviceConfig GetSyntheticDestinationDevice(string key, string name) + { + // If not, synthesize the device config + var devConf = new DeviceConfig + { + Group = "genericdestination", + Key = key, + Name = name, + Type = "genericdestination", + Properties = new JObject(new JProperty(_syntheticDeviceKey, true)), + }; + + return devConf; + } + + private void CreateDestinationList(BasicConfig co) + { + var useDestEnable = Eisc.BooleanOutput[JoinMap.UseDestinationEnable.JoinNumber].BoolValue; + + var newDl = new Dictionary(); + + for (uint i = 0; i < SupportedDisplayCount; i++) + { + var name = Eisc.StringOutput[JoinMap.DestinationNameJoinStart.JoinNumber + i].StringValue; + var routeType = Eisc.StringOutput[JoinMap.DestinationTypeJoinStart.JoinNumber + i].StringValue; + var key = Eisc.StringOutput[JoinMap.DestinationDeviceKeyJoinStart.JoinNumber + i].StringValue; + //var order = Eisc.UShortOutput[JoinMap.DestinationOrderJoinStart.JoinNumber + i].UShortValue; + var enabled = Eisc.BooleanOutput[JoinMap.DestinationIsEnabledJoinStart.JoinNumber + i].BoolValue; + + if (useDestEnable && !enabled) + { + continue; + } + + if (string.IsNullOrEmpty(key)) + { + continue; + } + + Debug.Console(0, this, "Adding destination {0} - {1}", key, name); + + eRoutingSignalType parsedType; + try + { + parsedType = (eRoutingSignalType)Enum.Parse(typeof(eRoutingSignalType), routeType, true); + } + catch + { + Debug.Console(0, this, "Error parsing destination type: {0}", routeType); + parsedType = eRoutingSignalType.AudioVideo; + } + + var newDli = new DestinationListItem + { + Name = name, + Order = (int)i, + SinkKey = key, + SinkType = parsedType, + }; + + if (!newDl.ContainsKey(key)) + { + newDl.Add(key, newDli); + } + else + { + newDl[key] = newDli; + } + + if (!_directRouteMessenger.DestinationList.ContainsKey(newDli.SinkKey)) + { + //add same DestinationListItem to dictionary for messenger in order to allow for correlation by index + _directRouteMessenger.DestinationList.Add(key, newDli); + } + else + { + _directRouteMessenger.DestinationList[key] = newDli; + } + + var existingDev = co.GetDeviceForKey(key); + + var syntheticDisplay = GetSyntheticDestinationDevice(key, name); + + if (existingDev != null) + { + Debug.Console(0, this, "Found device with key: {0} in Essentials.", key); + + if (existingDev.Properties.Value(_syntheticDeviceKey)) + { + Debug.Console(0, this, "Updating previous device config with new values"); + } + else + { + Debug.Console(0, this, "Using existing Essentials device (non synthetic)"); + } + } + else + { + co.Devices.Add(syntheticDisplay); + } + } + + if (!co.DestinationLists.ContainsKey("default")) + { + co.DestinationLists.Add("default", newDl); + } + else + { + co.DestinationLists["default"] = newDl; + } + + _directRouteMessenger.RegisterForDestinationPaths(); + } + + /// + /// Iterates device config and adds messengers as neede for each device type + /// + private void SetupDeviceMessengers() + { + DeviceMessengers = new Dictionary(); + + try + { + foreach (var device in ConfigReader.ConfigObject.Devices) + { + if (device.Group.Equals("simplmessenger")) + { + var props = + JsonConvert.DeserializeObject(device.Properties.ToString()); + + var messengerKey = string.Format("device-{0}-{1}", Key, Key); + + if (DeviceManager.GetDeviceForKey(messengerKey) != null) + { + Debug.Console(2, this, "Messenger with key: {0} already exists. Skipping...", messengerKey); + continue; + } + + var dev = ConfigReader.ConfigObject.GetDeviceForKey(props.DeviceKey); + + if (dev == null) + { + Debug.Console(1, this, "Unable to find device config for key: '{0}'", props.DeviceKey); + continue; + } + + var type = device.Type.ToLower(); + MessengerBase messenger = null; + + if (type.Equals("simplcameramessenger")) + { + Debug.Console(2, this, "Adding SIMPLCameraMessenger for: '{0}'", props.DeviceKey); + messenger = new SIMPLCameraMessenger(messengerKey, Eisc, "/device/" + props.DeviceKey, + props.JoinStart); + } + else if (type.Equals("simplroutemessenger")) + { + Debug.Console(2, this, "Adding SIMPLRouteMessenger for: '{0}'", props.DeviceKey); + messenger = new SIMPLRouteMessenger(messengerKey, Eisc, "/device/" + props.DeviceKey, + props.JoinStart); + } + + if (messenger != null) + { + DeviceManager.AddDevice(messenger); + DeviceMessengers.Add(device.Key, messenger); + messenger.RegisterWithAppServer(Parent); + } + else + { + Debug.Console(2, this, "Unable to add messenger for device: '{0}' of type: '{1}'", + props.DeviceKey, type); + } + } + else + { + var dev = DeviceManager.GetDeviceForKey(device.Key); + + if (dev != null) + { + if (dev is CameraBase) + { + var camDevice = dev as CameraBase; + Debug.Console(1, this, "Adding CameraBaseMessenger for device: {0}", dev.Key); + var cameraMessenger = new CameraBaseMessenger(device.Key + "-" + Key, camDevice, + "/device/" + device.Key); + DeviceMessengers.Add(device.Key, cameraMessenger); + DeviceManager.AddDevice(cameraMessenger); + cameraMessenger.RegisterWithAppServer(Parent); + continue; + } + } + } + } + } + catch (Exception e) + { + Debug.Console(2, this, "Error Setting up Device Managers: {0}", e); + } + } + + /// + /// + /// + private void SendFullStatus() + { + if (ConfigIsLoaded) + { + var count = Eisc.UShortOutput[JoinMap.NumberOfAuxFaders.JoinNumber].UShortValue; + + Debug.Console(1, this, "The Fader Count is : {0}", count); + + // build volumes object, serialize and put in content of method below + + // Create auxFaders + var auxFaderDict = new Dictionary(); + + var volumeStart = JoinMap.VolumeJoinStart.JoinNumber; + + for (var i = volumeStart; i <= count; i++) + { + auxFaderDict.Add("level-" + i, + new Volume("level-" + i, + Eisc.UShortOutput[i].UShortValue, + Eisc.BooleanOutput[i].BoolValue, + Eisc.StringOutput[i].StringValue, + true, + "someting.png")); + } + + var volumes = new Volumes + { + Master = new Volume("master", + Eisc.UShortOutput[JoinMap.MasterVolume.JoinNumber].UShortValue, + Eisc.BooleanOutput[JoinMap.MasterVolume.JoinNumber].BoolValue, + Eisc.StringOutput[JoinMap.MasterVolume.JoinNumber].StringValue, + true, + "something.png") + { + HasPrivacyMute = true, + PrivacyMuted = Eisc.BooleanOutput[JoinMap.PrivacyMute.JoinNumber].BoolValue + }, + AuxFaders = auxFaderDict, + NumberOfAuxFaders = Eisc.UShortInput[JoinMap.NumberOfAuxFaders.JoinNumber].UShortValue + }; + + // TODO: Add property to status message to indicate if advanced sharing is supported and if users can change share mode + + PostStatus(new + { + activityMode = GetActivityMode(), + isOn = Eisc.BooleanOutput[JoinMap.RoomIsOn.JoinNumber].BoolValue, + selectedSourceKey = Eisc.StringOutput[JoinMap.CurrentSourceKey.JoinNumber].StringValue, + volumes, + supportsAdvancedSharing = Eisc.BooleanOutput[JoinMap.SupportsAdvancedSharing.JoinNumber].BoolValue, + userCanChangeShareMode = Eisc.BooleanOutput[JoinMap.UserCanChangeShareMode.JoinNumber].BoolValue, + }); + } + else + { + PostStatus(new + { + error = "systemNotReady" + }); + } + } + + /// + /// Returns the activity mode int + /// + /// + private int GetActivityMode() + { + if (Eisc.BooleanOutput[JoinMap.ActivityPhoneCall.JoinNumber].BoolValue) return 2; + if (Eisc.BooleanOutput[JoinMap.ActivityShare.JoinNumber].BoolValue) return 1; + + return Eisc.BooleanOutput[JoinMap.ActivityVideoCall.JoinNumber].BoolValue ? 3 : 0; + } + + /// + /// Helper for posting status message + /// + /// The contents of the content object + private void PostStatus(object contentObject) + { + AppServerController.SendMessageObject(new MobileControlMessage + { + Type = "/status/", + Content = JToken.FromObject(contentObject) + }); + } + + /// + /// + /// + /// + /// + private void PostMessage(string messageType, object contentObject) + { + AppServerController.SendMessageObject(new MobileControlMessage + { + Type = messageType, + Content = JToken.FromObject(contentObject) + }); + } + + + /// + /// + /// + /// + /// + private void EISC_SigChange(object currentDevice, SigEventArgs args) + { + if (Debug.Level >= 1) + Debug.Console(1, this, "SIMPL EISC change: {0} {1}={2}", args.Sig.Type, args.Sig.Number, + args.Sig.StringValue); + var uo = args.Sig.UserObject; + if (uo != null) + { + if (uo is Action) + (uo as Action)(args.Sig.BoolValue); + else if (uo is Action) + (uo as Action)(args.Sig.UShortValue); + else if (uo is Action) + (uo as Action)(args.Sig.StringValue); + } + } + + /// + /// Returns the mapping of types to groups, for setting up devices. + /// + /// + private Dictionary GetSourceGroupDictionary() + { + //type, group + var d = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {"laptop", "pc"}, + {"pc", "pc"}, + {"wireless", "genericsource"}, + {"iptv", "settopbox"}, + {"simplcameramessenger", "simplmessenger"}, + {"camera", "camera"}, + + }; + return d; + } + + /// + /// updates the usercode from server + /// + protected override void UserCodeChange() + { + + Debug.Console(1, this, "Server user code changed: {0}", UserCode); + + var qrUrl = string.Format("{0}/api/rooms/{1}/{3}/qr?x={2}", AppServerController.Host, AppServerController.SystemUuid, new Random().Next(), "room1"); + QrCodeUrl = qrUrl; + + Debug.Console(1, this, "Server user code changed: {0} - {1}", UserCode, qrUrl); + + OnUserCodeChanged(); + + Eisc.StringInput[JoinMap.UserCodeToSystem.JoinNumber].StringValue = UserCode; + Eisc.StringInput[JoinMap.ServerUrl.JoinNumber].StringValue = McServerUrl; + Eisc.StringInput[JoinMap.QrCodeUrl.JoinNumber].StringValue = QrCodeUrl; + + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/SourceDeviceMapDictionary.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/SourceDeviceMapDictionary.cs new file mode 100644 index 00000000..84a024a8 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/SourceDeviceMapDictionary.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; + +namespace PepperDash.Essentials.Room.MobileControl +{ + /// + /// Contains all of the default joins that map to API funtions + /// + public class SourceDeviceMapDictionary : Dictionary + { + public SourceDeviceMapDictionary() + { + var dictionary = new Dictionary + { + {"preset01", 101}, + {"preset02", 102}, + {"preset03", 103}, + {"preset04", 104}, + {"preset05", 105}, + {"preset06", 106}, + {"preset07", 107}, + {"preset08", 108}, + {"preset09", 109}, + {"preset10", 110}, + {"preset11", 111}, + {"preset12", 112}, + {"preset13", 113}, + {"preset14", 114}, + {"preset15", 115}, + {"preset16", 116}, + {"preset17", 117}, + {"preset18", 118}, + {"preset19", 119}, + {"preset20", 120}, + {"preset21", 121}, + {"preset22", 122}, + {"preset23", 123}, + {"preset24", 124}, + {"num0", 130}, + {"num1", 131}, + {"num2", 132}, + {"num3", 133}, + {"num4", 134}, + {"num5", 135}, + {"num6", 136}, + {"num7", 137}, + {"num8", 138}, + {"num9", 139}, + {"numDash", 140}, + {"numEnter", 141}, + {"chanUp", 142}, + {"chanDown", 143}, + {"lastChan", 144}, + {"exit", 145}, + {"powerToggle", 146}, + {"red", 147}, + {"green", 148}, + {"yellow", 149}, + {"blue", 150}, + {"video", 151}, + {"previous", 152}, + {"next", 153}, + {"rewind", 154}, + {"ffwd", 155}, + {"closedCaption", 156}, + {"stop", 157}, + {"pause", 158}, + {"up", 159}, + {"down", 160}, + {"left", 161}, + {"right", 162}, + {"settings", 163}, + {"info", 164}, + {"return", 165}, + {"guide", 166}, + {"reboot", 167}, + {"dvrList", 168}, + {"replay", 169}, + {"play", 170}, + {"select", 171}, + {"record", 172}, + {"menu", 173}, + {"topMenu", 174}, + {"prevTrack", 175}, + {"nextTrack", 176}, + {"powerOn", 177}, + {"powerOff", 178}, + {"dot", 179} + }; + + foreach (var item in dictionary) + { + Add(item.Key, item.Value); + } + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs new file mode 100644 index 00000000..00ae00b0 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs @@ -0,0 +1,76 @@ +using PepperDash.Core; +using System; +using System.Net.Http; +using System.Threading.Tasks; + +namespace PepperDash.Essentials.Services +{ + + public class MobileControlApiService + { + private readonly HttpClient _client; + + public MobileControlApiService(string apiUrl) + { + var handler = new HttpClientHandler + { + AllowAutoRedirect = false, + ServerCertificateCustomValidationCallback = (req, cert, certChain, errors) => true + }; + + _client = new HttpClient(handler); + } + + public async Task SendAuthorizationRequest(string apiUrl, string grantCode, string systemUuid) + { + try + { + var request = new HttpRequestMessage(HttpMethod.Get, $"{apiUrl}/system/{systemUuid}/authorize?grantCode={grantCode}"); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Sending authorization request to {host}", null, request.RequestUri); + + var response = await _client.SendAsync(request); + + var authResponse = new AuthorizationResponse + { + Authorized = response.StatusCode == System.Net.HttpStatusCode.OK + }; + + if (authResponse.Authorized) + { + return authResponse; + } + + if (response.StatusCode == System.Net.HttpStatusCode.Moved) + { + var location = response.Headers.Location; + + authResponse.Reason = $"ERROR: Mobile Control API has moved. Please adjust configuration to \"{location}\""; + + return authResponse; + } + + var responseString = await response.Content.ReadAsStringAsync(); + + switch (responseString) + { + case "codeNotFound": + authResponse.Reason = $"Authorization failed. Code not found for system UUID {systemUuid}"; + break; + case "uuidNotFound": + authResponse.Reason = $"Authorization failed. System UUID {systemUuid} not found. Check Essentials configuration."; + break; + default: + authResponse.Reason = $"Authorization failed. Response {response.StatusCode}: {responseString}"; + break; + } + + return authResponse; + } catch(Exception ex) + { + Debug.LogMessage(ex, "Error authorizing with Mobile Control"); + return new AuthorizationResponse { Authorized = false, Reason = ex.Message }; + } + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs new file mode 100644 index 00000000..3d6eb7b9 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs @@ -0,0 +1,17 @@ +using PepperDash.Core; +using PepperDash.Essentials.Core; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PepperDash.Essentials.Touchpanel +{ + public interface ITheme:IKeyed + { + string Theme { get; } + + void UpdateTheme(string theme); + } +} diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs new file mode 100644 index 00000000..814b51c6 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControl.cs @@ -0,0 +1,25 @@ +using PepperDash.Core; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Touchpanel +{ + public interface ITswAppControl : IKeyed + { + BoolFeedback AppOpenFeedback { get; } + + void HideOpenApp(); + + void CloseOpenApp(); + + void OpenApp(); + } + + public interface ITswZoomControl : IKeyed + { + BoolFeedback ZoomIncomingCallFeedback { get; } + + BoolFeedback ZoomInCallFeedback { get; } + + void EndZoomCall(); + } +} diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs new file mode 100644 index 00000000..52fa693b --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; + +namespace PepperDash.Essentials.Touchpanel +{ + public class ITswAppControlMessenger : MessengerBase + { + private readonly ITswAppControl _appControl; + + public ITswAppControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + { + _appControl = device as ITswAppControl; + } + + protected override void RegisterActions() + { + if (_appControl == null) + { + Debug.Console(0, this, $"{_device.Key} does not implement ITswAppControl"); + return; + } + + AddAction($"/fullStatus", (id, context) => SendFullStatus()); + + AddAction($"/openApp", (id, context) => _appControl.OpenApp()); + + AddAction($"/closeApp", (id, context) => _appControl.CloseOpenApp()); + + AddAction($"/hideApp", (id, context) => _appControl.HideOpenApp()); + + _appControl.AppOpenFeedback.OutputChange += (s, a) => + { + PostStatusMessage(JToken.FromObject(new + { + appOpen = a.BoolValue + })); + }; + } + + private void SendFullStatus() + { + var message = new TswAppStateMessage + { + AppOpen = _appControl.AppOpenFeedback.BoolValue, + }; + + PostStatusMessage(message); + } + } + + public class TswAppStateMessage : DeviceStateMessageBase + { + [JsonProperty("appOpen", NullValueHandling = NullValueHandling.Ignore)] + public bool? AppOpen { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs new file mode 100644 index 00000000..da59d93c --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs @@ -0,0 +1,73 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; + + +namespace PepperDash.Essentials.Touchpanel +{ + public class ITswZoomControlMessenger : MessengerBase + { + private readonly ITswZoomControl _zoomControl; + + public ITswZoomControlMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + { + _zoomControl = device as ITswZoomControl; + } + + protected override void RegisterActions() + { + if (_zoomControl == null) + { + Debug.Console(0, this, $"{_device.Key} does not implement ITswZoomControl"); + return; + } + + AddAction($"/fullStatus", (id, context) => SendFullStatus()); + + + AddAction($"/endCall", (id, context) => _zoomControl.EndZoomCall()); + + _zoomControl.ZoomIncomingCallFeedback.OutputChange += (s, a) => + { + PostStatusMessage(JToken.FromObject(new + { + incomingCall = a.BoolValue, + inCall = _zoomControl.ZoomInCallFeedback.BoolValue + })); + }; + + + _zoomControl.ZoomInCallFeedback.OutputChange += (s, a) => + { + PostStatusMessage(JToken.FromObject( + new + { + inCall = a.BoolValue, + incomingCall = _zoomControl.ZoomIncomingCallFeedback.BoolValue + })); + }; + } + + private void SendFullStatus() + { + var message = new TswZoomStateMessage + { + InCall = _zoomControl?.ZoomInCallFeedback.BoolValue, + IncomingCall = _zoomControl?.ZoomIncomingCallFeedback.BoolValue + }; + + PostStatusMessage(message); + } + } + + public class TswZoomStateMessage : DeviceStateMessageBase + { + [JsonProperty("inCall", NullValueHandling = NullValueHandling.Ignore)] + public bool? InCall { get; set; } + + [JsonProperty("incomingCall", NullValueHandling = NullValueHandling.Ignore)] + public bool? IncomingCall { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs new file mode 100644 index 00000000..703cb291 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -0,0 +1,571 @@ +using Crestron.SimplSharpPro; +using Crestron.SimplSharpPro.DeviceSupport; +using Crestron.SimplSharpPro.UI; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.Core.DeviceInfo; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.UI; +using PepperDash.Essentials.Touchpanel; +using System; +using System.Collections.Generic; +using System.Linq; +using Feedback = PepperDash.Essentials.Core.Feedback; + +namespace PepperDash.Essentials.Devices.Common.TouchPanel +{ + //public interface IMobileControlTouchpanelController + //{ + // StringFeedback AppUrlFeedback { get; } + // string DefaultRoomKey { get; } + // string DeviceKey { get; } + //} + + + public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlTouchpanelController, ITheme + { + private readonly MobileControlTouchpanelProperties localConfig; + private IMobileControlRoomMessenger _bridge; + + private string _appUrl; + + public StringFeedback AppUrlFeedback { get; private set; } + private readonly StringFeedback QrCodeUrlFeedback; + private readonly StringFeedback McServerUrlFeedback; + private readonly StringFeedback UserCodeFeedback; + + private readonly BoolFeedback _appOpenFeedback; + + public BoolFeedback AppOpenFeedback => _appOpenFeedback; + + private readonly BoolFeedback _zoomIncomingCallFeedback; + + public BoolFeedback ZoomIncomingCallFeedback => _zoomIncomingCallFeedback; + + private readonly BoolFeedback _zoomInCallFeedback; + + public event DeviceInfoChangeHandler DeviceInfoChanged; + + public BoolFeedback ZoomInCallFeedback => _zoomInCallFeedback; + + + public FeedbackCollection Feedbacks { get; private set; } + + public FeedbackCollection ZoomFeedbacks { get; private set; } + + public string DefaultRoomKey => _config.DefaultRoomKey; + + public bool UseDirectServer => localConfig.UseDirectServer; + + public bool ZoomRoomController => localConfig.ZoomRoomController; + + public string Theme => localConfig.Theme; + + public StringFeedback ThemeFeedback { get; private set; } + + public DeviceInfo DeviceInfo => new DeviceInfo(); + + public MobileControlTouchpanelController(string key, string name, BasicTriListWithSmartObject panel, MobileControlTouchpanelProperties config) : base(key, name, panel, config) + { + localConfig = config; + + AddPostActivationAction(SubscribeForMobileControlUpdates); + + ThemeFeedback = new StringFeedback($"{Key}-theme",() => Theme); + AppUrlFeedback = new StringFeedback($"{Key}-appUrl", () => _appUrl); + QrCodeUrlFeedback = new StringFeedback($"{Key}-qrCodeUrl", () => _bridge?.QrCodeUrl); + McServerUrlFeedback = new StringFeedback($"{Key}-mcServerUrl", () => _bridge?.McServerUrl); + UserCodeFeedback = new StringFeedback($"{Key}-userCode", () => _bridge?.UserCode); + + _appOpenFeedback = new BoolFeedback($"{Key}-appOpen", () => + { + if (Panel is TswX60BaseClass tsX60) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, this, $"x60 sending {tsX60.ExtenderApplicationControlReservedSigs.HideOpenApplicationFeedback.BoolValue}"); + return !tsX60.ExtenderApplicationControlReservedSigs.HideOpenApplicationFeedback.BoolValue; + } + + if (Panel is TswX70Base tsX70) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, this, $"x70 sending {tsX70.ExtenderApplicationControlReservedSigs.HideOpenedApplicationFeedback.BoolValue}"); + return !tsX70.ExtenderApplicationControlReservedSigs.HideOpenedApplicationFeedback.BoolValue; + } + + return false; + }); + + _zoomIncomingCallFeedback = new BoolFeedback($"{Key}-zoomIncomingCall", () => + { + if (Panel is TswX60WithZoomRoomAppReservedSigs tsX60) + { + return tsX60.ExtenderZoomRoomAppReservedSigs.ZoomRoomIncomingCallFeedback.BoolValue; + } + + if (Panel is TswX70Base tsX70) + { + return tsX70.ExtenderZoomRoomAppReservedSigs.ZoomRoomIncomingCallFeedback.BoolValue; + } + + return false; + }); + + _zoomInCallFeedback = new BoolFeedback($"{Key}-zoomInCall", () => + { + if (Panel is TswX60WithZoomRoomAppReservedSigs tsX60) + { + return tsX60.ExtenderZoomRoomAppReservedSigs.ZoomRoomActiveFeedback.BoolValue; + } + + if (Panel is TswX70Base tsX70) + { + return tsX70.ExtenderZoomRoomAppReservedSigs.ZoomRoomActiveFeedback.BoolValue; + } + + return false; + }); + + Feedbacks = new FeedbackCollection + { + AppUrlFeedback, QrCodeUrlFeedback, McServerUrlFeedback, UserCodeFeedback + }; + + ZoomFeedbacks = new FeedbackCollection { + AppOpenFeedback, _zoomInCallFeedback, _zoomIncomingCallFeedback + }; + + RegisterForExtenders(); + } + + public void UpdateTheme(string theme) + { + localConfig.Theme = theme; + + var props = JToken.FromObject(localConfig); + + var deviceConfig = ConfigReader.ConfigObject.Devices.FirstOrDefault((d) => d.Key == Key); + + if (deviceConfig == null) { return; } + + deviceConfig.Properties = props; + + ConfigWriter.UpdateDeviceConfig(deviceConfig); + } + + private void RegisterForExtenders() + { + if (Panel is TswXX70Base x70Panel) + { + x70Panel.ExtenderApplicationControlReservedSigs.DeviceExtenderSigChange += (e, a) => + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, this, $"X70 App Control Device Extender args: {a.Event}:{a.Sig}:{a.Sig.Type}:{a.Sig.BoolValue}:{a.Sig.UShortValue}:{a.Sig.StringValue}"); + + UpdateZoomFeedbacks(); + + if (!x70Panel.ExtenderApplicationControlReservedSigs.HideOpenedApplicationFeedback.BoolValue) + { + x70Panel.ExtenderButtonToolbarReservedSigs.ShowButtonToolbar(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button2On(); + } + else + { + x70Panel.ExtenderButtonToolbarReservedSigs.HideButtonToolbar(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button2Off(); + } + }; + + + x70Panel.ExtenderZoomRoomAppReservedSigs.DeviceExtenderSigChange += (e, a) => + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, this, $"X70 Zoom Room Ap Device Extender args: {a.Event}:{a.Sig}:{a.Sig.Type}:{a.Sig.BoolValue}:{a.Sig.UShortValue}:{a.Sig.StringValue}"); + + if (a.Sig.Number == x70Panel.ExtenderZoomRoomAppReservedSigs.ZoomRoomIncomingCallFeedback.Number) + { + ZoomIncomingCallFeedback.FireUpdate(); + } + else if (a.Sig.Number == x70Panel.ExtenderZoomRoomAppReservedSigs.ZoomRoomActiveFeedback.Number) + { + ZoomInCallFeedback.FireUpdate(); + } + }; + + + x70Panel.ExtenderEthernetReservedSigs.DeviceExtenderSigChange += (e, a) => + { + DeviceInfo.MacAddress = x70Panel.ExtenderEthernetReservedSigs.MacAddressFeedback.StringValue; + DeviceInfo.IpAddress = x70Panel.ExtenderEthernetReservedSigs.IpAddressFeedback.StringValue; + + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, this, $"MAC: {DeviceInfo.MacAddress} IP: {DeviceInfo.IpAddress}"); + + var handler = DeviceInfoChanged; + + if (handler == null) + { + return; + } + + handler(this, new DeviceInfoEventArgs(DeviceInfo)); + }; + + x70Panel.ExtenderApplicationControlReservedSigs.Use(); + x70Panel.ExtenderZoomRoomAppReservedSigs.Use(); + x70Panel.ExtenderEthernetReservedSigs.Use(); + x70Panel.ExtenderButtonToolbarReservedSigs.Use(); + + x70Panel.ExtenderButtonToolbarReservedSigs.Button1Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button3Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button4Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button5Off(); + x70Panel.ExtenderButtonToolbarReservedSigs.Button6Off(); + + return; + } + + if (Panel is TswX60WithZoomRoomAppReservedSigs x60withZoomApp) + { + x60withZoomApp.ExtenderApplicationControlReservedSigs.DeviceExtenderSigChange += (e, a) => + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, this, $"X60 App Control Device Extender args: {a.Event}:{a.Sig}:{a.Sig.Type}:{a.Sig.BoolValue}:{a.Sig.UShortValue}:{a.Sig.StringValue}"); + + if (a.Sig.Number == x60withZoomApp.ExtenderApplicationControlReservedSigs.HideOpenApplicationFeedback.Number) + { + AppOpenFeedback.FireUpdate(); + } + }; + x60withZoomApp.ExtenderZoomRoomAppReservedSigs.DeviceExtenderSigChange += (e, a) => + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, this, $"X60 Zoom Room App Device Extender args: {a.Event}:{a.Sig}:{a.Sig.Type}:{a.Sig.BoolValue}:{a.Sig.UShortValue}:{a.Sig.StringValue}"); + + if (a.Sig.Number == x60withZoomApp.ExtenderZoomRoomAppReservedSigs.ZoomRoomIncomingCallFeedback.Number) + { + ZoomIncomingCallFeedback.FireUpdate(); + } + else if (a.Sig.Number == x60withZoomApp.ExtenderZoomRoomAppReservedSigs.ZoomRoomActiveFeedback.Number) + { + ZoomInCallFeedback.FireUpdate(); + } + }; + + x60withZoomApp.ExtenderEthernetReservedSigs.DeviceExtenderSigChange += (e, a) => + { + DeviceInfo.MacAddress = x60withZoomApp.ExtenderEthernetReservedSigs.MacAddressFeedback.StringValue; + DeviceInfo.IpAddress = x60withZoomApp.ExtenderEthernetReservedSigs.IpAddressFeedback.StringValue; + + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, this, $"MAC: {DeviceInfo.MacAddress} IP: {DeviceInfo.IpAddress}"); + + var handler = DeviceInfoChanged; + + if (handler == null) + { + return; + } + + handler(this, new DeviceInfoEventArgs(DeviceInfo)); + }; + + x60withZoomApp.ExtenderZoomRoomAppReservedSigs.Use(); + x60withZoomApp.ExtenderApplicationControlReservedSigs.Use(); + x60withZoomApp.ExtenderEthernetReservedSigs.Use(); + } + } + + public override bool CustomActivate() + { + var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this); + + var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this); + + var themeMessenger = new ThemeMessenger($"themeMessenger-{Key}", $"/device/{Key}", this); + + var mc = DeviceManager.AllDevices.OfType().FirstOrDefault(); + + if (mc == null) + { + return base.CustomActivate(); + } + + if (!(Panel is TswXX70Base) && !(Panel is TswX60WithZoomRoomAppReservedSigs)) + { + mc.AddDeviceMessenger(themeMessenger); + + return base.CustomActivate(); + } + + mc.AddDeviceMessenger(appMessenger); + mc.AddDeviceMessenger(zoomMessenger); + mc.AddDeviceMessenger(themeMessenger); + + return base.CustomActivate(); + } + + + protected override void ExtenderSystemReservedSigs_DeviceExtenderSigChange(DeviceExtender currentDeviceExtender, SigEventArgs args) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, this, $"System Device Extender args: ${args.Event}:${args.Sig}"); + } + + protected override void SetupPanelDrivers(string roomKey) + { + AppUrlFeedback.LinkInputSig(Panel.StringInput[1]); + QrCodeUrlFeedback.LinkInputSig(Panel.StringInput[2]); + McServerUrlFeedback.LinkInputSig(Panel.StringInput[3]); + UserCodeFeedback.LinkInputSig(Panel.StringInput[4]); + + Panel.OnlineStatusChange += (sender, args) => + { + UpdateFeedbacks(); + + this.LogInformation("Sending {appUrl} on join 1", AppUrlFeedback.StringValue); + + Panel.StringInput[1].StringValue = AppUrlFeedback.StringValue; + Panel.StringInput[2].StringValue = QrCodeUrlFeedback.StringValue; + Panel.StringInput[3].StringValue = McServerUrlFeedback.StringValue; + Panel.StringInput[4].StringValue = UserCodeFeedback.StringValue; + }; + } + + private void SubscribeForMobileControlUpdates() + { + foreach (var dev in DeviceManager.AllDevices) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, this, $"{dev.Key}:{dev.GetType().Name}"); + } + + var mcList = DeviceManager.AllDevices.OfType().ToList(); + + if (mcList.Count == 0) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, this, $"No Mobile Control controller found"); + + return; + } + + // use first in list, since there should only be one. + var mc = mcList[0]; + + var bridge = mc.GetRoomBridge(_config.DefaultRoomKey); + + if (bridge == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, this, $"No Mobile Control bridge for {_config.DefaultRoomKey} found "); + return; + } + + _bridge = bridge; + + _bridge.UserCodeChanged += UpdateFeedbacks; + _bridge.AppUrlChanged += (s, a) => { + this.LogInformation("AppURL changed"); + SetAppUrl(_bridge.AppUrl); + UpdateFeedbacks(s, a); + }; + + SetAppUrl(_bridge.AppUrl); + } + + public void SetAppUrl(string url) + { + _appUrl = url; + AppUrlFeedback.FireUpdate(); + } + + private void UpdateFeedbacks(object sender, EventArgs args) + { + UpdateFeedbacks(); + } + + private void UpdateFeedbacks() + { + foreach (var feedback in Feedbacks) { this.LogDebug("Updating {feedbackKey}", feedback.Key); feedback.FireUpdate(); } + } + + private void UpdateZoomFeedbacks() + { + foreach (var feedback in ZoomFeedbacks) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, this, $"Updating {feedback.Key}"); + feedback.FireUpdate(); + } + } + + public void HideOpenApp() + { + if (Panel is TswX70Base x70Panel) + { + x70Panel.ExtenderApplicationControlReservedSigs.HideOpenedApplication(); + return; + } + + if (Panel is TswX60BaseClass x60Panel) + { + x60Panel.ExtenderApplicationControlReservedSigs.HideOpenApplication(); + return; + } + } + + public void OpenApp() + { + if (Panel is TswX70Base x70Panel) + { + x70Panel.ExtenderApplicationControlReservedSigs.OpenApplication(); + return; + } + + if (Panel is TswX60WithZoomRoomAppReservedSigs) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, this, $"X60 panel does not support zoom app"); + return; + } + } + + public void CloseOpenApp() + { + if (Panel is TswX70Base x70Panel) + { + x70Panel.ExtenderApplicationControlReservedSigs.CloseOpenedApplication(); + return; + } + + if (Panel is TswX60WithZoomRoomAppReservedSigs x60Panel) + { + x60Panel.ExtenderApplicationControlReservedSigs.CloseOpenedApplication(); + return; + } + } + + public void EndZoomCall() + { + if (Panel is TswX70Base x70Panel) + { + x70Panel.ExtenderZoomRoomAppReservedSigs.ZoomRoomEndCall(); + return; + } + + if (Panel is TswX60WithZoomRoomAppReservedSigs x60Panel) + { + x60Panel.ExtenderZoomRoomAppReservedSigs.ZoomRoomEndCall(); + return; + } + } + + public void UpdateDeviceInfo() + { + if (Panel is TswXX70Base x70Panel) + { + DeviceInfo.MacAddress = x70Panel.ExtenderEthernetReservedSigs.MacAddressFeedback.StringValue; + DeviceInfo.IpAddress = x70Panel.ExtenderEthernetReservedSigs.IpAddressFeedback.StringValue; + + var handler = DeviceInfoChanged; + + if (handler == null) + { + return; + } + + handler(this, new DeviceInfoEventArgs(DeviceInfo)); + } + + if (Panel is TswX60WithZoomRoomAppReservedSigs x60Panel) + { + DeviceInfo.MacAddress = x60Panel.ExtenderEthernetReservedSigs.MacAddressFeedback.StringValue; + DeviceInfo.IpAddress = x60Panel.ExtenderEthernetReservedSigs.IpAddressFeedback.StringValue; + + var handler = DeviceInfoChanged; + + if (handler == null) + { + return; + } + + handler(this, new DeviceInfoEventArgs(DeviceInfo)); + } + + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, this, $"MAC: {DeviceInfo.MacAddress} IP: {DeviceInfo.IpAddress}"); + } + } + + public class MobileControlTouchpanelControllerFactory : EssentialsPluginDeviceFactory + { + public MobileControlTouchpanelControllerFactory() + { + TypeNames = new List() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel" }; + MinimumEssentialsFrameworkVersion = "2.0.0"; + } + + public override EssentialsDevice BuildDevice(DeviceConfig dc) + { + var comm = CommFactory.GetControlPropertiesConfig(dc); + var props = JsonConvert.DeserializeObject(dc.Properties.ToString()); + + var panel = GetPanelForType(dc.Type, comm.IpIdInt, props.ProjectName); + + if (panel == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Unable to create Touchpanel for type {0}. Touchpanel Controller WILL NOT function correctly", dc.Type); + } + + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Factory Attempting to create new MobileControlTouchpanelController"); + + var panelController = new MobileControlTouchpanelController(dc.Key, dc.Name, panel, props); + + return panelController; + } + + private BasicTriListWithSmartObject GetPanelForType(string type, uint id, string projectName) + { + type = type.ToLower().Replace("mc", ""); + try + { + if (type == "crestronapp") + { + var app = new CrestronApp(id, Global.ControlSystem); + app.ParameterProjectName.Value = projectName; + return app; + } + else if (type == "xpanel") + return new XpanelForHtml5(id, Global.ControlSystem); + else if (type == "tsw550") + return new Tsw550(id, Global.ControlSystem); + else if (type == "tsw552") + return new Tsw552(id, Global.ControlSystem); + else if (type == "tsw560") + return new Tsw560(id, Global.ControlSystem); + else if (type == "tsw750") + return new Tsw750(id, Global.ControlSystem); + else if (type == "tsw752") + return new Tsw752(id, Global.ControlSystem); + else if (type == "tsw760") + return new Tsw760(id, Global.ControlSystem); + else if (type == "tsw1050") + return new Tsw1050(id, Global.ControlSystem); + else if (type == "tsw1052") + return new Tsw1052(id, Global.ControlSystem); + else if (type == "tsw1060") + return new Tsw1060(id, Global.ControlSystem); + else if (type == "tsw570") + return new Tsw570(id, Global.ControlSystem); + else if (type == "tsw770") + return new Tsw770(id, Global.ControlSystem); + else if (type == "ts770") + return new Ts770(id, Global.ControlSystem); + else if (type == "tsw1070") + return new Tsw1070(id, Global.ControlSystem); + else if (type == "ts1070") + return new Ts1070(id, Global.ControlSystem); + else + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "WARNING: Cannot create TSW controller with type '{0}'", type); + return null; + } + } + catch (Exception e) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "WARNING: Cannot create TSW base class. Panel will not function: {0}", e.Message); + return null; + } + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs new file mode 100644 index 00000000..1d8f8ebf --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials.Devices.Common.TouchPanel +{ + public class MobileControlTouchpanelProperties : CrestronTouchpanelPropertiesConfig + { + [JsonProperty("useDirectServer")] + public bool UseDirectServer { get; set; } = false; + + [JsonProperty("zoomRoomController")] + public bool ZoomRoomController { get; set; } = false; + + [JsonProperty("buttonToolbarTimeoutInS")] + public ushort ButtonToolbarTimoutInS { get; set; } = 0; + + [JsonProperty("theme")] + public string Theme { get; set; } = "light"; + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs new file mode 100644 index 00000000..cf93197c --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.AppServer; +using PepperDash.Essentials.AppServer.Messengers; + +namespace PepperDash.Essentials.Touchpanel +{ + public class ThemeMessenger : MessengerBase + { + private readonly ITheme _tpDevice; + + public ThemeMessenger(string key, string path, ITheme device) : base(key, path, device as Device) + { + _tpDevice = device; + } + + protected override void RegisterActions() + { + AddAction("/fullStatus", (id, content) => + { + PostStatusMessage(new ThemeUpdateMessage { Theme = _tpDevice.Theme }); + }); + + AddAction("/saveTheme", (id, content) => + { + var theme = content.ToObject>(); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Setting theme to {theme}", this, theme.Value); + _tpDevice.UpdateTheme(theme.Value); + + PostStatusMessage(JToken.FromObject(new {theme = theme.Value})); + }); + } + } + + public class ThemeUpdateMessage:DeviceStateMessageBase + { + [JsonProperty("theme")] + public string Theme { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs new file mode 100644 index 00000000..912d6af1 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs @@ -0,0 +1,150 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core.Queues; +using System; +using System.Threading; +using WebSocketSharp; + +namespace PepperDash.Essentials +{ + public class TransmitMessage : IQueueMessage + { + private readonly WebSocket _ws; + private readonly object msgToSend; + + public TransmitMessage(object msg, WebSocket ws) + { + _ws = ws; + msgToSend = msg; + } + + public TransmitMessage(DeviceStateMessageBase msg, WebSocket ws) + { + _ws = ws; + msgToSend = msg; + } + + #region Implementation of IQueueMessage + + public void Dispatch() + { + try + { + + //Debug.Console(2, "Dispatching message type: {0}", msgToSend.GetType()); + + //Debug.Console(2, "Message: {0}", msgToSend.ToString()); + + //var messageToSend = JObject.FromObject(msgToSend); + + if (_ws != null && _ws.IsAlive) + { + var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); + + Debug.Console(2, "Message TX: {0}", message); + + _ws.Send(message); + } + else if (_ws == null) + { + Debug.Console(1, "Cannot send. No client."); + } + } + catch (Exception ex) + { + Debug.Console(0, Debug.ErrorLogLevel.Error, "Caught an exception in the Transmit Processor {0}\r{1}\r{2}", ex.Message, ex.InnerException, ex.StackTrace); + Debug.Console(2, Debug.ErrorLogLevel.Error, "Stack Trace: {0}", ex.StackTrace); + + if (ex.InnerException != null) + { + Debug.Console(0, Debug.ErrorLogLevel.Error, "Inner Exception: {0}", ex.InnerException.Message); + Debug.Console(2, Debug.ErrorLogLevel.Error, "Stack Trace: {0}", ex.InnerException.StackTrace); + } + } + + + } + #endregion + } + + +#if SERIES4 + public class MessageToClients : IQueueMessage + { + private readonly MobileControlWebsocketServer _server; + private readonly object msgToSend; + + public MessageToClients(object msg, MobileControlWebsocketServer server) + { + _server = server; + msgToSend = msg; + } + + public MessageToClients(DeviceStateMessageBase msg, MobileControlWebsocketServer server) + { + _server = server; + msgToSend = msg; + } + + #region Implementation of IQueueMessage + + public void Dispatch() + { + try + { + //Debug.Console(2, "Message: {0}", msgToSend.ToString()); + + if (_server != null) + { + Debug.Console(2, _server, Debug.ErrorLogLevel.Notice, "Dispatching message type: {0}", msgToSend.GetType()); + + var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); + + var clientSpecificMessage = msgToSend as MobileControlMessage; + if (clientSpecificMessage.ClientId != null) + { + var clientId = clientSpecificMessage.ClientId; + + Debug.Console(2, _server, "Message TX To Client ID: {0} Message: {1}", clientId, message); + + _server.SendMessageToClient(clientId, message); + } + else + { + _server.SendMessageToAllClients(message); + + Debug.Console(2, "Message TX To Clients: {0}", message); + } + } + else if (_server == null) + { + Debug.Console(1, "Cannot send. No server."); + } + } + catch (ThreadAbortException) + { + //Swallowing this exception, as it occurs on shutdown and there's no need to print out a scary stack trace + } + catch (Exception ex) + { + Debug.Console(0, Debug.ErrorLogLevel.Error, "Caught an exception in the Transmit Processor {0}", ex.Message); + Debug.Console(2, Debug.ErrorLogLevel.Error, "Stack Trace: {0}", ex.StackTrace); + + if (ex.InnerException != null) + { + Debug.Console(0, Debug.ErrorLogLevel.Error, "----\r\n{0}", ex.InnerException.Message); + Debug.Console(2, Debug.ErrorLogLevel.Error, "Stack Trace: {0}", ex.InnerException.StackTrace); + } + } + + + } + #endregion + } + +#endif +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs b/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs new file mode 100644 index 00000000..ef004421 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace PepperDash.Essentials.AppServer +{ + public class UserCodeChangedContent + { + [JsonProperty("userCode")] + public string UserCode { get; set; } + + [JsonProperty("qrChecksum", NullValueHandling = NullValueHandling.Include)] + public string QrChecksum { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/Volumes.cs b/src/PepperDash.Essentials.MobileControl/Volumes.cs new file mode 100644 index 00000000..88556675 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/Volumes.cs @@ -0,0 +1,76 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace PepperDash.Essentials.Room.MobileControl +{ + public class Volumes + { + [JsonProperty("master", NullValueHandling = NullValueHandling.Ignore)] + public Volume Master { get; set; } + + [JsonProperty("auxFaders", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary AuxFaders { get; set; } + + [JsonProperty("numberOfAuxFaders", NullValueHandling = NullValueHandling.Ignore)] + public int? NumberOfAuxFaders { get; set; } + + public Volumes() + { + } + } + + public class Volume + { + [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] + public string Key { get; set; } + + [JsonProperty("level", NullValueHandling = NullValueHandling.Ignore)] + public int? Level { get; set; } + + [JsonProperty("muted", NullValueHandling = NullValueHandling.Ignore)] + public bool? Muted { get; set; } + + [JsonProperty("label", NullValueHandling = NullValueHandling.Ignore)] + public string Label { get; set; } + + [JsonProperty("hasMute", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasMute { get; set; } + + [JsonProperty("hasPrivacyMute", NullValueHandling = NullValueHandling.Ignore)] + public bool? HasPrivacyMute { get; set; } + + [JsonProperty("privacyMuted", NullValueHandling = NullValueHandling.Ignore)] + public bool? PrivacyMuted { get; set; } + + + [JsonProperty("muteIcon", NullValueHandling = NullValueHandling.Ignore)] + public string MuteIcon { get; set; } + + public Volume(string key, int level, bool muted, string label, bool hasMute, string muteIcon) + : this(key) + { + Level = level; + Muted = muted; + Label = label; + HasMute = hasMute; + MuteIcon = muteIcon; + } + + public Volume(string key, int level) + : this(key) + { + Level = level; + } + + public Volume(string key, bool muted) + : this(key) + { + Muted = muted; + } + + public Volume(string key) + { + Key = key; + } + } +} \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs new file mode 100644 index 00000000..7351cc1e --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs @@ -0,0 +1,52 @@ +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core.Web.RequestHandlers; +using PepperDash.Essentials.AppServer.Messengers; +using System.Collections.Generic; +using System.Linq; + +namespace PepperDash.Essentials.WebApiHandlers +{ + public class ActionPathsHandler : WebApiBaseRequestHandler + { + private readonly MobileControlSystemController mcController; + public ActionPathsHandler(MobileControlSystemController controller) : base(true) + { + mcController = controller; + } + + protected override void HandleGet(HttpCwsContext context) + { + var response = JsonConvert.SerializeObject(new ActionPathsResponse(mcController)); + + context.Response.StatusCode = 200; + context.Response.ContentType = "application/json"; + context.Response.Headers.Add("Content-Type", "application/json"); + context.Response.Write(response, false); + context.Response.End(); + } + } + + public class ActionPathsResponse + { + [JsonIgnore] + private readonly MobileControlSystemController mcController; + + [JsonProperty("actionPaths")] + public List ActionPaths => mcController.GetActionDictionaryPaths().Select((path) => new ActionPath { MessengerKey = path.Item1, Path = path.Item2}).ToList(); + + public ActionPathsResponse(MobileControlSystemController mcController) + { + this.mcController = mcController; + } + } + + public class ActionPath + { + [JsonProperty("messengerKey")] + public string MessengerKey { get; set; } + + [JsonProperty("path")] + public string Path { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs new file mode 100644 index 00000000..4f2774f4 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileAuthRequestHandler.cs @@ -0,0 +1,59 @@ +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Web.RequestHandlers; +using PepperDash.Essentials.Core.Web; +using System; +using System.Threading.Tasks; + +namespace PepperDash.Essentials.WebApiHandlers +{ + public class MobileAuthRequestHandler : WebApiBaseRequestAsyncHandler + { + private readonly MobileControlSystemController mcController; + + public MobileAuthRequestHandler(MobileControlSystemController controller) : base(true) + { + mcController = controller; + } + + protected override async Task HandlePost(HttpCwsContext context) + { + try + { + var requestBody = EssentialsWebApiHelpers.GetRequestBody(context.Request); + + var grantCode = JsonConvert.DeserializeObject(requestBody); + + if (string.IsNullOrEmpty(grantCode?.GrantCode)) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Missing grant code"); + context.Response.StatusCode = 400; + context.Response.StatusDescription = "Missing grant code"; + context.Response.End(); + return; + } + + var response = await mcController.ApiService.SendAuthorizationRequest(mcController.Host, grantCode.GrantCode, mcController.SystemUuid); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, $"response received"); + if (response.Authorized) + { + mcController.RegisterSystemToServer(); + } + + + context.Response.StatusCode = 200; + var responseBody = JsonConvert.SerializeObject(response, Formatting.None); + context.Response.ContentType = "application/json"; + context.Response.Headers.Add("Content-Type", "application/json"); + context.Response.Write(responseBody, false); + context.Response.End(); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Exception recieved authorizing system"); + } + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs new file mode 100644 index 00000000..f0c1809c --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -0,0 +1,159 @@ +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Web.RequestHandlers; +using PepperDash.Essentials.Core.Config; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace PepperDash.Essentials.WebApiHandlers +{ + public class MobileInfoHandler : WebApiBaseRequestHandler + { + private readonly MobileControlSystemController mcController; + public MobileInfoHandler(MobileControlSystemController controller) : base(true) + { + mcController = controller; + } + + protected override void HandleGet(HttpCwsContext context) + { + try + { + var response = new InformationResponse(mcController); + + context.Response.StatusCode = 200; + context.Response.ContentType = "application/json"; + context.Response.Write(JsonConvert.SerializeObject(response), false); + context.Response.End(); + } + catch (Exception ex) + { + Debug.Console(1, $"exception showing mobile info: {ex.Message}"); + Debug.Console(2, $"stack trace: {ex.StackTrace}"); + + context.Response.StatusCode = 500; + context.Response.End(); + } + } + } + + public class InformationResponse + { + [JsonIgnore] + private readonly MobileControlSystemController mcController; + + [JsonProperty("edgeServer", NullValueHandling = NullValueHandling.Ignore)] + public MobileControlEdgeServer EdgeServer => mcController.Config.EnableApiServer ? new MobileControlEdgeServer(mcController) : null; + + + [JsonProperty("directServer", NullValueHandling = NullValueHandling.Ignore)] + public MobileControlDirectServer DirectServer => mcController.Config.DirectServer.EnableDirectServer ? new MobileControlDirectServer(mcController.DirectServer) : null; + + + public InformationResponse(MobileControlSystemController controller) + { + mcController = controller; + } + } + + public class MobileControlEdgeServer + { + [JsonIgnore] + private readonly MobileControlSystemController mcController; + + [JsonProperty("serverAddress")] + public string ServerAddress => mcController.Config == null ? "No Config" : mcController.Host; + + [JsonProperty("systemName")] + public string SystemName => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].RoomName : "No Config"; + + [JsonProperty("systemUrl")] + public string SystemUrl => ConfigReader.ConfigObject.SystemUrl; + + [JsonProperty("userCode")] + public string UserCode => mcController.RoomBridges.Count > 0 ? mcController.RoomBridges[0].UserCode : "Not available"; + + [JsonProperty("connected")] + public bool Connected => mcController.Connected; + + [JsonProperty("secondsSinceLastAck")] + public int SecondsSinceLastAck => (DateTime.Now - mcController.LastAckMessage).Seconds; + + public MobileControlEdgeServer(MobileControlSystemController controller) + { + mcController = controller; + } + } + + public class MobileControlDirectServer + { + [JsonIgnore] + private readonly MobileControlWebsocketServer directServer; + + [JsonProperty("userAppUrl")] + public string UserAppUrl => $"{directServer.UserAppUrlPrefix}/[insert_client_token]"; + + [JsonProperty("serverPort")] + public int ServerPort => directServer.Port; + + [JsonProperty("tokensDefined")] + public int TokensDefined => directServer.UiClients.Count; + + [JsonProperty("clientsConnected")] + public int ClientsConnected => directServer.ConnectedUiClientsCount; + + [JsonProperty("clients")] + public List Clients => directServer.UiClients.Select((c, i) => { return new MobileControlDirectClient(c, i, directServer.UserAppUrlPrefix); }).ToList(); + + public MobileControlDirectServer(MobileControlWebsocketServer server) + { + directServer = server; + } + } + + public class MobileControlDirectClient + { + [JsonIgnore] + private readonly UiClientContext context; + + [JsonIgnore] + private readonly string Key; + + [JsonIgnore] + private readonly int clientNumber; + + [JsonIgnore] + private readonly string urlPrefix; + + [JsonProperty("clientNumber")] + public string ClientNumber => $"{clientNumber}"; + + [JsonProperty("roomKey")] + public string RoomKey => context.Token.RoomKey; + + [JsonProperty("touchpanelKey")] + public string TouchpanelKey => context.Token.TouchpanelKey; + + [JsonProperty("url")] + public string Url => $"{urlPrefix}{Key}"; + + [JsonProperty("token")] + public string Token => Key; + + [JsonProperty("connected")] + public bool Connected => context.Client == null ? false : context.Client.Context.WebSocket.IsAlive; + + [JsonProperty("duration")] + public double Duration => context.Client == null ? 0 : context.Client.ConnectedDuration.TotalSeconds; + + public MobileControlDirectClient(KeyValuePair clientContext, int index, string urlPrefix) + { + context = clientContext.Value; + Key = clientContext.Key; + clientNumber = index; + this.urlPrefix = urlPrefix; + } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs new file mode 100644 index 00000000..537dafb9 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs @@ -0,0 +1,164 @@ +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using PepperDash.Core; +using PepperDash.Core.Web.RequestHandlers; +using PepperDash.Essentials.Core.Web; + +namespace PepperDash.Essentials.WebApiHandlers +{ + public class UiClientHandler : WebApiBaseRequestHandler + { + private readonly MobileControlWebsocketServer server; + public UiClientHandler(MobileControlWebsocketServer directServer) : base(true) + { + server = directServer; + } + + protected override void HandlePost(HttpCwsContext context) + { + var req = context.Request; + var res = context.Response; + var body = EssentialsWebApiHelpers.GetRequestBody(req); + + var request = JsonConvert.DeserializeObject(body); + + var response = new ClientResponse(); + + if (string.IsNullOrEmpty(request?.RoomKey)) + { + response.Error = "roomKey is required"; + + res.StatusCode = 400; + res.ContentType = "application/json"; + res.Headers.Add("Content-Type", "application/json"); + res.Write(JsonConvert.SerializeObject(response), false); + res.End(); + return; + } + + if (string.IsNullOrEmpty(request.GrantCode)) + { + response.Error = "grantCode is required"; + + res.StatusCode = 400; + res.ContentType = "application/json"; + res.Headers.Add("Content-Type", "application/json"); + res.Write(JsonConvert.SerializeObject(response), false); + res.End(); + return; + } + + var (token, path) = server.ValidateGrantCode(request.GrantCode, request.RoomKey); + + response.Token = token; + response.Path = path; + + res.StatusCode = 200; + res.ContentType = "application/json"; + res.Headers.Add("Content-Type", "application/json"); + res.Write(JsonConvert.SerializeObject(response), false); + res.End(); + } + + protected override void HandleDelete(HttpCwsContext context) + { + var req = context.Request; + var res = context.Response; + var body = EssentialsWebApiHelpers.GetRequestBody(req); + + var request = JsonConvert.DeserializeObject(body); + + + + if (string.IsNullOrEmpty(request?.Token)) + { + var response = new ClientResponse + { + Error = "token is required" + }; + + res.StatusCode = 400; + res.ContentType = "application/json"; + res.Headers.Add("Content-Type", "application/json"); + res.Write(JsonConvert.SerializeObject(response), false); + res.End(); + + return; + } + + + + if (!server.UiClients.TryGetValue(request.Token, out UiClientContext clientContext)) + { + var response = new ClientResponse + { + Error = $"Unable to find client with token: {request.Token}" + }; + + res.StatusCode = 200; + res.ContentType = "application/json"; + res.Headers.Add("Content-Type", "application/json"); + res.Write(JsonConvert.SerializeObject(response), false); + res.End(); + + return; + } + + if (clientContext.Client != null && clientContext.Client.Context.WebSocket.IsAlive) + { + clientContext.Client.Context.WebSocket.Close(WebSocketSharp.CloseStatusCode.Normal, "Token removed from server"); + } + + var path = server.WsPath + request.Token; + + if (!server.Server.RemoveWebSocketService(path)) + { + Debug.Console(0, $"Unable to remove client with token {request.Token}"); + + var response = new ClientResponse + { + Error = $"Unable to remove client with token {request.Token}" + }; + + res.StatusCode = 500; + res.ContentType = "application/json"; + res.Headers.Add("Content-Type", "application/json"); + res.Write(JsonConvert.SerializeObject(response), false); + res.End(); + + return; + } + + server.UiClients.Remove(request.Token); + + server.UpdateSecret(); + + res.StatusCode = 200; + res.End(); + } + } + + public class ClientRequest + { + [JsonProperty("roomKey", NullValueHandling = NullValueHandling.Ignore)] + public string RoomKey { get; set; } + + [JsonProperty("grantCode", NullValueHandling = NullValueHandling.Ignore)] + public string GrantCode { get; set; } + + [JsonProperty("token", NullValueHandling = NullValueHandling.Ignore)] + public string Token { get; set; } + } + + public class ClientResponse + { + [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] + public string Error { get; set; } + + [JsonProperty("token", NullValueHandling = NullValueHandling.Ignore)] + public string Token { get; set; } + + [JsonProperty("path", NullValueHandling = NullValueHandling.Ignore)] + public string Path { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs new file mode 100644 index 00000000..b1aa483e --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -0,0 +1,1404 @@ +using Crestron.SimplSharp; +using Crestron.SimplSharp.WebScripting; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using PepperDash.Core; +using PepperDash.Essentials.AppServer.Messengers; +using PepperDash.Essentials.Core; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; +using PepperDash.Essentials.Core.Web; +using PepperDash.Essentials.WebApiHandlers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using WebSocketSharp; +using WebSocketSharp.Net; +using WebSocketSharp.Server; +using ErrorEventArgs = WebSocketSharp.ErrorEventArgs; + + +namespace PepperDash.Essentials +{ + /// + /// Represents the behaviour to associate with a UiClient for WebSocket communication + /// + public class UiClient : WebSocketBehavior + { + public MobileControlSystemController Controller { get; set; } + + public string RoomKey { get; set; } + + private string _clientId; + + private DateTime _connectionTime; + + public TimeSpan ConnectedDuration + { + get + { + if (Context.WebSocket.IsAlive) + { + return DateTime.Now - _connectionTime; + } + else + { + return new TimeSpan(0); + } + } + } + + public UiClient() + { + + } + + protected override void OnOpen() + { + base.OnOpen(); + + var url = Context.WebSocket.Url; + Debug.Console(2, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url); + + var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); + + if (!match.Success) + { + _connectionTime = DateTime.Now; + return; + } + + var clientId = match.Groups[1].Value; + _clientId = clientId; + + if (Controller == null) + { + Debug.Console(2, "WebSocket UiClient Controller is null"); + _connectionTime = DateTime.Now; + } + + var clientJoinedMessage = new MobileControlMessage + { + Type = "/system/clientJoined", + Content = JToken.FromObject(new + { + clientId, + roomKey = RoomKey, + }) + }; + + Controller.HandleClientMessage(JsonConvert.SerializeObject(clientJoinedMessage)); + // Inform controller of client joining + /* + var clientJoined = new MobileControlMessage + { + Type = "/system/roomKey", + ClientId = clientId, + Content = RoomKey, + }; + + Controller.SendMessageObjectToDirectClient(clientJoined); + + if (Controller.Config.EnableUiMirroring) + { + var uiMirrorEnabled = new MobileControlMessage + { + Type = "/system/uiMirrorEnabled", + ClientId = clientId, + Content = JToken.FromObject(new MobileControlSimpleContent { Value = Controller.Config.EnableUiMirroring }), + }; + + var uiState = new MobileControlMessage + { + Type = "/system/uiMirrorState", + ClientId = clientId, + Content = Controller.LastUiState, + }; + + Controller.SendMessageObjectToDirectClient(uiMirrorEnabled); + + Controller.SendMessageObjectToDirectClient(uiState); + }*/ + + var bridge = Controller.GetRoomBridge(RoomKey); + + if (bridge == null) return; + + SendUserCodeToClient(bridge, clientId); + + bridge.UserCodeChanged -= Bridge_UserCodeChanged; + bridge.UserCodeChanged += Bridge_UserCodeChanged; + + // TODO: Future: Check token to see if there's already an open session using that token and reject/close the session + } + + private void Bridge_UserCodeChanged(object sender, EventArgs e) + { + SendUserCodeToClient((MobileControlEssentialsRoomBridge)sender, _clientId); + } + + private void SendUserCodeToClient(MobileControlBridgeBase bridge, string clientId) + { + var content = new + { + userCode = bridge.UserCode, + qrUrl = bridge.QrCodeUrl, + }; + + var message = new MobileControlMessage + { + Type = "/system/userCodeChanged", + ClientId = clientId, + Content = JToken.FromObject(content) + }; + + Controller.SendMessageObjectToDirectClient(message); + } + + protected override void OnMessage(MessageEventArgs e) + { + base.OnMessage(e); + + if (e.IsText && e.Data.Length > 0 && Controller != null) + { + // Forward the message to the controller to be put on the receive queue + Controller.HandleClientMessage(e.Data); + } + } + + protected override void OnClose(CloseEventArgs e) + { + base.OnClose(e); + + Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); + + } + + protected override void OnError(ErrorEventArgs e) + { + base.OnError(e); + + Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); + } + } + + public class MobileControlWebsocketServer : EssentialsDevice + { + private readonly string userAppPath = Global.FilePathPrefix + "mcUserApp" + Global.DirectorySeparator; + + private readonly string localConfigFolderName = "_local-config"; + + private readonly string appConfigFileName = "_config.local.json"; + + /// + /// Where the key is the join token and the value is the room key + /// + //private Dictionary _joinTokens; + + private HttpServer _server; + + public HttpServer Server => _server; + + public Dictionary UiClients { get; private set; } + + private readonly MobileControlSystemController _parent; + + private WebSocketServerSecretProvider _secretProvider; + + private ServerTokenSecrets _secret; + + private static readonly HttpClient LogClient = new HttpClient(); + + private string SecretProviderKey + { + get + { + return string.Format("{0}:{1}-tokens", Global.ControlSystem.ProgramNumber, Key); + } + } + + /// + /// The path for the WebSocket messaging + /// + private readonly string _wsPath = "/mc/api/ui/join/"; + + public string WsPath => _wsPath; + + /// + /// The path to the location of the files for the user app (single page Angular app) + /// + private readonly string _appPath = string.Format("{0}mcUserApp", Global.FilePathPrefix); + + /// + /// The base HREF that the user app uses + /// + private string _userAppBaseHref = "/mc/app"; + + /// + /// The prot the server will run on + /// + public int Port { get; private set; } + + public string UserAppUrlPrefix + { + get + { + return string.Format("http://{0}:{1}{2}?token=", + CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), + Port, + _userAppBaseHref); + + } + } + + public int ConnectedUiClientsCount + { + get + { + var count = 0; + + foreach (var client in UiClients) + { + if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) + { + count++; + } + } + + return count; + } + } + + public MobileControlWebsocketServer(string key, int customPort, MobileControlSystemController parent) + : base(key) + { + _parent = parent; + + // Set the default port to be 50000 plus the slot number of the program + Port = 50000 + (int)Global.ControlSystem.ProgramNumber; + + if (customPort != 0) + { + Port = customPort; + } + + if(parent.Config.DirectServer.AutomaticallyForwardPortToCSLAN == true) + { + try + { + CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter); + + + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Automatically forwarding port {0} to CS LAN", Port); + + var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter); + var csIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); + + var result = CrestronEthernetHelper.AddPortForwarding((ushort)Port, (ushort)Port, csIp, CrestronEthernetHelper.ePortMapTransport.TCP); + + if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Error adding port forwarding: {0}", result); + } + } + catch (ArgumentException) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "This processor does not have a CS LAN", this); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Error automatically forwarding port to CS LAN"); + } + } + + + UiClients = new Dictionary(); + + //_joinTokens = new Dictionary(); + + if (Global.Platform == eDevicePlatform.Appliance) + { + AddConsoleCommands(); + } + + AddPreActivationAction(() => AddWebApiPaths()); + } + + private void AddWebApiPaths() + { + var apiServer = DeviceManager.AllDevices.OfType().FirstOrDefault(); + + if (apiServer == null) + { + Debug.Console(0, this, "No API Server available"); + return; + } + + var routes = new List + { + new HttpCwsRoute($"devices/{Key}/client") + { + Name = "ClientHandler", + RouteHandler = new UiClientHandler(this) + }, + }; + + apiServer.AddRoute(routes); + } + + private void AddConsoleCommands() + { + CrestronConsole.AddNewConsoleCommand(GenerateClientTokenFromConsole, "MobileAddUiClient", "Adds a client and generates a token. ? for more help", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(RemoveToken, "MobileRemoveUiClient", "Removes a client. ? for more help", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand((s) => PrintClientInfo(), "MobileGetClientInfo", "Displays the current client info", ConsoleAccessLevelEnum.AccessOperator); + CrestronConsole.AddNewConsoleCommand(RemoveAllTokens, "MobileRemoveAllClients", "Removes all clients", ConsoleAccessLevelEnum.AccessOperator); + } + + + public override void Initialize() + { + try + { + base.Initialize(); + + _server = new HttpServer(Port, false); + + _server.OnGet += Server_OnGet; + + _server.OnOptions += Server_OnOptions; + + if (_parent.Config.DirectServer.Logging.EnableRemoteLogging) + { + _server.OnPost += Server_OnPost; + } + + CrestronEnvironment.ProgramStatusEventHandler += CrestronEnvironment_ProgramStatusEventHandler; + + _server.Start(); + + if (_server.IsListening) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Mobile Control WebSocket Server listening on port {port}", this, _server.Port); + } + + CrestronEnvironment.ProgramStatusEventHandler += OnProgramStop; + + RetrieveSecret(); + + CreateFolderStructure(); + + AddClientsForTouchpanels(); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Exception intializing websocket server", this); + } + } + + private void AddClientsForTouchpanels() + { + var touchpanels = DeviceManager.AllDevices + .OfType().Where(tp => tp.UseDirectServer); + + + var touchpanelsToAdd = new List(); + + if (_secret != null) + { + var newTouchpanels = touchpanels.Where(tp => !_secret.Tokens.Any(t => t.Value.TouchpanelKey != null && t.Value.TouchpanelKey.Equals(tp.Key, StringComparison.InvariantCultureIgnoreCase))); + + touchpanelsToAdd.AddRange(newTouchpanels); + } + else + { + touchpanelsToAdd.AddRange(touchpanels); + } + + foreach (var client in touchpanelsToAdd) + { + var bridge = _parent.GetRoomBridge(client.DefaultRoomKey); + + if (bridge == null) + { + Debug.Console(0, this, $"Unable to find room with key: {client.DefaultRoomKey}"); + return; + } + + var (key, path) = GenerateClientToken(bridge, client.Key); + + if (key == null) + { + Debug.Console(0, this, $"Unable to generate a client for {client.Key}"); + continue; + } + } + + var lanAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter); + + var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Processor IP: {processorIp}", this); + + foreach (var touchpanel in touchpanels.Select(tp => + { + var token = _secret.Tokens.FirstOrDefault((t) => t.Value.TouchpanelKey.Equals(tp.Key, StringComparison.InvariantCultureIgnoreCase)); + + var messenger = _parent.GetRoomBridge(tp.DefaultRoomKey); + + return new { token.Key, Touchpanel = tp, Messenger = messenger }; + })) + { + if (touchpanel.Key == null) + { + Debug.Console(0, this, $"Token for touchpanel {touchpanel.Touchpanel.Key} not found"); + continue; + } + + if (touchpanel.Messenger == null) + { + Debug.Console(2, this, $"Unable to find room messenger for {touchpanel.Touchpanel.DefaultRoomKey}"); + continue; + } + + var appUrl = $"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"; + + Debug.Console(2, this, $"Sending URL {appUrl}"); + + touchpanel.Messenger.UpdateAppUrl($"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"); + } + } + + private void OnProgramStop(eProgramStatusEventType programEventType) + { + switch (programEventType) + { + case eProgramStatusEventType.Stopping: + _server.Stop(); + break; + } + } + + private void CreateFolderStructure() + { + if (!Directory.Exists(userAppPath)) + { + Directory.CreateDirectory(userAppPath); + } + + if (!Directory.Exists($"{userAppPath}{localConfigFolderName}")) + { + Directory.CreateDirectory($"{userAppPath}{localConfigFolderName}"); + } + + using (var sw = new StreamWriter(File.Open($"{userAppPath}{localConfigFolderName}{Global.DirectorySeparator}{appConfigFileName}", FileMode.Create, FileAccess.ReadWrite))) + { + var config = GetApplicationConfig(); + + var contents = JsonConvert.SerializeObject(config, Formatting.Indented); + + sw.Write(contents); + } + } + + private MobileControlApplicationConfig GetApplicationConfig() + { + MobileControlApplicationConfig config = null; + + var lanAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetLANAdapter); + + var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId); + + try + { + if (_parent.Config.ApplicationConfig == null) + { + config = new MobileControlApplicationConfig + { + ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port), + GatewayAppPath = "", + LogoPath = "logo/logo.png", + EnableDev = false, + IconSet = MCIconSet.GOOGLE, + LoginMode = "room-list", + Modes = new Dictionary + { + { + "room-list", + new McMode{ + ListPageText= "Please select your room", + LoginHelpText = "Please select your room from the list, then enter the code shown on the display.", + PasscodePageText = "Please enter the code shown on this room's display" + } + } + }, + Logging = _parent.Config.DirectServer.Logging.EnableRemoteLogging, + }; + } + else + { + config = new MobileControlApplicationConfig + { + ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port), + GatewayAppPath = "", + LogoPath = _parent.Config.ApplicationConfig.LogoPath ?? "logo/logo.png", + EnableDev = _parent.Config.ApplicationConfig.EnableDev ?? false, + IconSet = _parent.Config.ApplicationConfig.IconSet ?? MCIconSet.GOOGLE, + LoginMode = _parent.Config.ApplicationConfig.LoginMode ?? "room-list", + Modes = _parent.Config.ApplicationConfig.Modes ?? new Dictionary + { + { + "room-list", + new McMode { + ListPageText = "Please select your room", + LoginHelpText = "Please select your room from the list, then enter the code shown on the display.", + PasscodePageText = "Please enter the code shown on this room's display" + } + } + }, + Logging = _parent.Config.ApplicationConfig.Logging, + PartnerMetadata = _parent.Config.ApplicationConfig.PartnerMetadata ?? new List() + }; + } + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Error getting application configuration", this); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Config Object: {config} from {parentConfig}", this, config, _parent.Config); + } + + return config; + } + + /// + /// Attempts to retrieve secrets previously stored in memory + /// + private void RetrieveSecret() + { + try + { + // Add secret provider + _secretProvider = new WebSocketServerSecretProvider(SecretProviderKey); + + // Check for existing secrets + var secret = _secretProvider.GetSecret(SecretProviderKey); + + if (secret != null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Secret successfully retrieved", this); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Secret: {0}", this, secret.Value.ToString()); + + + // populate the local secrets object + _secret = JsonConvert.DeserializeObject(secret.Value.ToString()); + + if (_secret != null && _secret.Tokens != null) + { + // populate the _uiClient collection + foreach (var token in _secret.Tokens) + { + if(token.Value == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Token value is null", this); + continue; + } + + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Adding token: {0} for room: {1}", this, token.Key, token.Value.RoomKey); + + if(UiClients == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "UiClients is null", this); + UiClients = new Dictionary(); + } + + UiClients.Add(token.Key, new UiClientContext(token.Value)); + } + } + + if (UiClients.Count > 0) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Restored {uiClientCount} UiClients from secrets data", this, UiClients.Count); + + foreach (var client in UiClients) + { + var key = client.Key; + var path = _wsPath + key; + var roomKey = client.Value.Token.RoomKey; + + _server.AddWebSocketService(path, () => + { + var c = new UiClient(); + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Constructing UiClient with id: {key}", this, key); + + c.Controller = _parent; + c.RoomKey = roomKey; + UiClients[key].SetClient(c); + return c; + }); + + + //_server.WebSocketServices.AddService(path, (c) => + //{ + // Debug.Console(2, this, "Constructing UiClient with id: {0}", key); + // c.Controller = _parent; + // c.RoomKey = roomKey; + // UiClients[key].SetClient(c); + //}); + } + } + } + else + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No secret found"); + } + + Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "{uiClientCount} UiClients restored from secrets data", this, UiClients.Count); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Exception retrieving secret", this); + } + } + + /// + /// Stores secrets to memory to persist through reboot + /// + public void UpdateSecret() + { + try + { + if (_secret == null) + { + Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Secret is null", this); + + _secret = new ServerTokenSecrets(string.Empty); + } + + _secret.Tokens.Clear(); + + foreach (var uiClientContext in UiClients) + { + _secret.Tokens.Add(uiClientContext.Key, uiClientContext.Value.Token); + } + + var serializedSecret = JsonConvert.SerializeObject(_secret); + + _secretProvider.SetSecret(SecretProviderKey, serializedSecret); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Exception updating secret", this); + } + } + + /// + /// Generates a new token based on validating a room key and grant code passed in. If valid, returns a token and adds a service to the server for that token's path + /// + /// + private void GenerateClientTokenFromConsole(string s) + { + if (s == "?" || string.IsNullOrEmpty(s)) + { + CrestronConsole.ConsoleCommandResponse(@"[RoomKey] [GrantCode] Validates the room key against the grant code and returns a token for use in a UI client"); + return; + } + + var values = s.Split(' '); + var roomKey = values[0]; + var grantCode = values[1]; + + var bridge = _parent.GetRoomBridge(roomKey); + + if (bridge == null) + { + CrestronConsole.ConsoleCommandResponse(string.Format("Unable to find room with key: {0}", roomKey)); + return; + } + + var (token, path) = ValidateGrantCode(grantCode, bridge); + + if (token == null) + { + CrestronConsole.ConsoleCommandResponse("Grant Code is not valid"); + return; + } + + CrestronConsole.ConsoleCommandResponse($"Added new WebSocket UiClient service at path: {path}"); + CrestronConsole.ConsoleCommandResponse($"Token: {token}"); + } + + public (string, string) ValidateGrantCode(string grantCode, string roomKey) + { + var bridge = _parent.GetRoomBridge(roomKey); + + if (bridge == null) + { + Debug.Console(0, this, $"Unable to find room with key: {roomKey}"); + return (null, null); + } + + return ValidateGrantCode(grantCode, bridge); + } + + public (string, string) ValidateGrantCode(string grantCode, MobileControlBridgeBase bridge) + { + // TODO: Authenticate grant code passed in + // For now, we just generate a random guid as the token and use it as the ClientId as well + var grantCodeIsValid = true; + + if (grantCodeIsValid) + { + if (_secret == null) + { + _secret = new ServerTokenSecrets(grantCode); + } + + return GenerateClientToken(bridge, ""); + } + else + { + return (null, null); + } + } + + public (string, string) GenerateClientToken(MobileControlBridgeBase bridge, string touchPanelKey = "") + { + var key = Guid.NewGuid().ToString(); + + var token = new JoinToken { Code = bridge.UserCode, RoomKey = bridge.RoomKey, Uuid = _parent.SystemUuid, TouchpanelKey = touchPanelKey }; + + UiClients.Add(key, new UiClientContext(token)); + + var path = _wsPath + key; + + _server.AddWebSocketService(path, () => + { + var c = new UiClient(); + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Constructing UiClient with id: {0}", this, key); + c.Controller = _parent; + c.RoomKey = bridge.RoomKey; + UiClients[key].SetClient(c); + return c; + }); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Added new WebSocket UiClient service at path: {path}", this, path); + Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Token: {@token}", this, token); + + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "{serviceCount} websocket services present", this, _server.WebSocketServices.Count); + + UpdateSecret(); + + return (key, path); + } + + /// + /// Removes all clients from the server + /// + private void RemoveAllTokens(string s) + { + if (s == "?" || string.IsNullOrEmpty(s)) + { + CrestronConsole.ConsoleCommandResponse(@"Removes all clients from the server. To execute add 'confirm' to command"); + return; + } + + if (s != "confirm") + { + CrestronConsole.ConsoleCommandResponse(@"To remove all clients, add 'confirm' to the command"); + return; + } + + foreach (var client in UiClients) + { + if (client.Value.Client != null && client.Value.Client.Context.WebSocket.IsAlive) + { + client.Value.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down"); + } + + var path = _wsPath + client.Key; + if (_server.RemoveWebSocketService(path)) + { + CrestronConsole.ConsoleCommandResponse(string.Format("Client removed with token: {0}", client.Key)); + } + else + { + CrestronConsole.ConsoleCommandResponse(string.Format("Unable to remove client with token : {0}", client.Key)); + } + } + + UiClients.Clear(); + + UpdateSecret(); + } + + /// + /// Removes a client with the specified token value + /// + /// + private void RemoveToken(string s) + { + if (s == "?" || string.IsNullOrEmpty(s)) + { + CrestronConsole.ConsoleCommandResponse(@"[token] Removes the client with the specified token value"); + return; + } + + var key = s; + + if (UiClients.ContainsKey(key)) + { + var uiClientContext = UiClients[key]; + + if (uiClientContext.Client != null && uiClientContext.Client.Context.WebSocket.IsAlive) + { + uiClientContext.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Token removed from server"); + } + + var path = _wsPath + key; + if (_server.RemoveWebSocketService(path)) + { + UiClients.Remove(key); + + UpdateSecret(); + + CrestronConsole.ConsoleCommandResponse(string.Format("Client removed with token: {0}", key)); + } + else + { + CrestronConsole.ConsoleCommandResponse(string.Format("Unable to remove client with token : {0}", key)); + } + } + else + { + CrestronConsole.ConsoleCommandResponse(string.Format("Unable to find client with token: {0}", key)); + } + } + + /// + /// Prints out info about current client IDs + /// + private void PrintClientInfo() + { + CrestronConsole.ConsoleCommandResponse("Mobile Control UI Client Info:\r"); + + CrestronConsole.ConsoleCommandResponse(string.Format("{0} clients found:\r", UiClients.Count)); + + foreach (var client in UiClients) + { + CrestronConsole.ConsoleCommandResponse(string.Format("RoomKey: {0} Token: {1}\r", client.Value.Token.RoomKey, client.Key)); + } + } + + private void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) + { + if (programEventType == eProgramStatusEventType.Stopping) + { + foreach (var client in UiClients.Values) + { + if (client.Client != null && client.Client.Context.WebSocket.IsAlive) + { + client.Client.Context.WebSocket.Close(CloseStatusCode.Normal, "Server Shutting Down"); + } + } + + StopServer(); + } + } + + /// + /// Handler for GET requests to server + /// + /// + /// + private void Server_OnGet(object sender, HttpRequestEventArgs e) + { + try + { + var req = e.Request; + var res = e.Response; + res.ContentEncoding = Encoding.UTF8; + + res.AddHeader("Access-Control-Allow-Origin", "*"); + + var path = req.RawUrl; + + Debug.Console(2, this, "GET Request received at path: {0}", path); + + // Call for user app to join the room with a token + if (path.StartsWith("/mc/api/ui/joinroom")) + { + HandleJoinRequest(req, res); + } + // Call to get the server version + else if (path.StartsWith("/mc/api/version")) + { + HandleVersionRequest(res); + } + else if (path.StartsWith("/mc/app/logo")) + { + HandleImageRequest(req, res); + } + // Call to serve the user app + else if (path.StartsWith(_userAppBaseHref)) + { + HandleUserAppRequest(req, res, path); + } + else + { + // All other paths + res.StatusCode = 404; + res.Close(); + } + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Caught an exception in the OnGet handler", this); + } + } + + private async void Server_OnPost(object sender, HttpRequestEventArgs e) + { + try + { + var req = e.Request; + var res = e.Response; + + res.AddHeader("Access-Control-Allow-Origin", "*"); + + var path = req.RawUrl; + var ip = req.RemoteEndPoint.Address.ToString(); + + Debug.Console(2, this, "POST Request received at path: {0} from host {1}", path, ip); + + var body = new System.IO.StreamReader(req.InputStream).ReadToEnd(); + + if (path.StartsWith("/mc/api/log")) + { + res.StatusCode = 200; + res.Close(); + + var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs") + { + Content = new StringContent(body, Encoding.UTF8, "application/json"), + }; + + logRequest.Headers.Add("x-pepperdash-host", ip); + + await LogClient.SendAsync(logRequest); + + Debug.Console(2, this, "Log data sent to {0}:{1}", _parent.Config.DirectServer.Logging.Host, _parent.Config.DirectServer.Logging.Port); + } + else + { + res.StatusCode = 404; + res.Close(); + } + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Caught an exception in the OnPost handler", this); + } + } + + private void Server_OnOptions(object sender, HttpRequestEventArgs e) + { + try + { + var res = e.Response; + + res.AddHeader("Access-Control-Allow-Origin", "*"); + res.AddHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); + res.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me"); + + res.StatusCode = 200; + res.Close(); + } + catch (Exception ex) + { + Debug.LogMessage(ex, "Caught an exception in the OnPost handler", this); + } + } + + /// + /// Handle the request to join the room with a token + /// + /// + /// + private void HandleJoinRequest(HttpListenerRequest req, HttpListenerResponse res) + { + var qp = req.QueryString; + var token = qp["token"]; + + Debug.Console(2, this, "Join Room Request with token: {0}", token); + + + if (UiClients.TryGetValue(token, out UiClientContext clientContext)) + { + var bridge = _parent.GetRoomBridge(clientContext.Token.RoomKey); + + if (bridge != null) + { + res.StatusCode = 200; + res.ContentType = "application/json"; + + // Construct the response object + JoinResponse jRes = new JoinResponse + { + ClientId = token, + RoomKey = bridge.RoomKey, + SystemUuid = _parent.SystemUuid, + RoomUuid = _parent.SystemUuid, + Config = _parent.GetConfigWithPluginVersion(), + CodeExpires = new DateTime().AddYears(1), + UserCode = bridge.UserCode, + UserAppUrl = string.Format("http://{0}:{1}/mc/app", + CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0), + Port), + EnableDebug = false + }; + + // Serialize to JSON and convert to Byte[] + var json = JsonConvert.SerializeObject(jRes); + var body = Encoding.UTF8.GetBytes(json); + res.ContentLength64 = body.LongLength; + + // Send the response + res.Close(body, true); + } + else + { + var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey); + res.StatusCode = 404; + res.ContentType = "application/json"; + var body = Encoding.UTF8.GetBytes(message); + res.ContentLength64 = body.LongLength; + res.Close(body, true); + Debug.Console(2, this, "{0}", message); + } + } + else + { + var message = "Token invalid or has expired"; + res.StatusCode = 401; + res.ContentType = "application/json"; + Debug.Console(2, this, "{0}", message); + var body = Encoding.UTF8.GetBytes(message); + res.ContentLength64 = body.LongLength; + res.Close(body, true); + } + } + + /// + /// Handles a server version request + /// + /// + private void HandleVersionRequest(HttpListenerResponse res) + { + res.StatusCode = 200; + res.ContentType = "application/json"; + var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion }; + var message = JsonConvert.SerializeObject(version); + Debug.Console(2, this, "{0}", message); + + var body = Encoding.UTF8.GetBytes(message); + res.ContentLength64 = body.LongLength; + res.Close(body, true); + } + + /// + /// Handler to return images requested by the user app + /// + /// + /// + private void HandleImageRequest(HttpListenerRequest req, HttpListenerResponse res) + { + var path = req.RawUrl; + + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Requesting Image: {0}", this, path); + + var imageBasePath = Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo" + Global.DirectorySeparator; + + var image = path.Split('/').Last(); + + var filePath = imageBasePath + image; + + Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Retrieving Image: {0}", this, filePath); + + if (System.IO.File.Exists(filePath)) + { + if(filePath.EndsWith(".png")) + { + res.ContentType = "image/png"; + } + else if(filePath.EndsWith(".jpg")) + { + res.ContentType = "image/jpeg"; + } + else if(filePath.EndsWith(".gif")) + { + res.ContentType = "image/gif"; + } + else if(filePath.EndsWith(".svg")) + { + res.ContentType = "image/svg+xml"; + } + byte[] contents = System.IO.File.ReadAllBytes(filePath); + res.ContentLength64 = contents.LongLength; + res.Close(contents, true); + } + else + { + res.StatusCode = (int)HttpStatusCode.NotFound; + res.Close(); + } + } + + /// + /// Handles requests to serve files for the Angular single page app + /// + /// + /// + /// + private void HandleUserAppRequest(HttpListenerRequest req, HttpListenerResponse res, string path) + { + Debug.Console(2, this, "Requesting User app file..."); + + var qp = req.QueryString; + var token = qp["token"]; + + string filePath = path.Split('?')[0]; + + // remove the token from the path if found + //string filePath = path.Replace(string.Format("?token={0}", token), ""); + + // if there's no file suffix strip any extra path data after the base href + if (filePath != _userAppBaseHref && !filePath.Contains(".") && (!filePath.EndsWith(_userAppBaseHref) || !filePath.EndsWith(_userAppBaseHref += "/"))) + { + var suffix = filePath.Substring(_userAppBaseHref.Length, filePath.Length - _userAppBaseHref.Length); + if (suffix != "/") + { + //Debug.Console(2, this, "Suffix: {0}", suffix); + filePath = filePath.Replace(suffix, ""); + } + } + + // swap the base href prefix for the file path prefix + filePath = filePath.Replace(_userAppBaseHref, _appPath); + + Debug.Console(2, this, "filepath: {0}", filePath); + + + // append index.html if no specific file is specified + if (!filePath.Contains(".")) + { + if (filePath.EndsWith("/")) + { + filePath += "index.html"; + } + else + { + filePath += "/index.html"; + } + } + + // Set ContentType based on file type + if (filePath.EndsWith(".html")) + { + Debug.Console(2, this, "Client requesting User App..."); + + res.ContentType = "text/html"; + } + else + { + if (path.EndsWith(".js")) + { + res.ContentType = "application/javascript"; + } + else if (path.EndsWith(".css")) + { + res.ContentType = "text/css"; + } + else if (path.EndsWith(".json")) + { + res.ContentType = "application/json"; + } + } + + Debug.Console(2, this, "Attempting to serve file: {0}", filePath); + + byte[] contents; + if (System.IO.File.Exists(filePath)) + { + Debug.Console(2, this, "File found"); + contents = System.IO.File.ReadAllBytes(filePath); + } + else + { + Debug.Console(2, this, "File not found: {0}", filePath); + res.StatusCode = (int)HttpStatusCode.NotFound; + res.Close(); + return; + } + + res.ContentLength64 = contents.LongLength; + res.Close(contents, true); + } + + public void StopServer() + { + Debug.Console(2, this, "Stopping WebSocket Server"); + _server.Stop(CloseStatusCode.Normal, "Server Shutting Down"); + } + + /// + /// Sends a message to all connectd clients + /// + /// + public void SendMessageToAllClients(string message) + { + foreach (var clientContext in UiClients.Values) + { + if (clientContext.Client != null && clientContext.Client.Context.WebSocket.IsAlive) + { + clientContext.Client.Context.WebSocket.Send(message); + } + } + } + + /// + /// Sends a message to a specific client + /// + /// + /// + public void SendMessageToClient(object clientId, string message) + { + if (clientId == null) + { + return; + } + + if (UiClients.TryGetValue((string)clientId, out UiClientContext clientContext)) + { + if (clientContext.Client != null) + { + var socket = clientContext.Client.Context.WebSocket; + + if (socket.IsAlive) + { + socket.Send(message); + } + } + } + else + { + Debug.Console(0, this, "Unable to find client with ID: {0}", clientId); + } + } + } + + /// + /// Class to describe the server version info + /// + public class Version + { + [JsonProperty("serverVersion")] + public string ServerVersion { get; set; } + + [JsonProperty("serverIsRunningOnProcessorHardware")] + public bool ServerIsRunningOnProcessorHardware { get; private set; } + + public Version() + { + ServerIsRunningOnProcessorHardware = true; + } + } + + /// + /// Represents an instance of a UiClient and the associated Token + /// + public class UiClientContext + { + public UiClient Client { get; private set; } + public JoinToken Token { get; private set; } + + public UiClientContext(JoinToken token) + { + Token = token; + } + + public void SetClient(UiClient client) + { + Client = client; + } + + } + + /// + /// Represents the data structure for the grant code and UiClient tokens to be stored in the secrets manager + /// + public class ServerTokenSecrets + { + public string GrantCode { get; set; } + + public Dictionary Tokens { get; set; } + + public ServerTokenSecrets(string grantCode) + { + GrantCode = grantCode; + Tokens = new Dictionary(); + } + } + + /// + /// Represents a join token with the associated properties + /// + public class JoinToken + { + public string Code { get; set; } + + public string RoomKey { get; set; } + + public string Uuid { get; set; } + + public string TouchpanelKey { get; set; } = ""; + + public string Token { get; set; } = null; + } + + /// + /// Represents the structure of the join response + /// + public class JoinResponse + { + [JsonProperty("clientId")] + public string ClientId { get; set; } + + [JsonProperty("roomKey")] + public string RoomKey { get; set; } + + [JsonProperty("systemUUid")] + public string SystemUuid { get; set; } + + [JsonProperty("roomUUid")] + public string RoomUuid { get; set; } + + [JsonProperty("config")] + public object Config { get; set; } + + [JsonProperty("codeExpires")] + public DateTime CodeExpires { get; set; } + + [JsonProperty("userCode")] + public string UserCode { get; set; } + + [JsonProperty("userAppUrl")] + public string UserAppUrl { get; set; } + + [JsonProperty("enableDebug")] + public bool EnableDebug { get; set; } + } +} diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs new file mode 100644 index 00000000..7aa40996 --- /dev/null +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs @@ -0,0 +1,37 @@ +using Newtonsoft.Json; +using PepperDash.Essentials.Core; + +namespace PepperDash.Essentials +{ + internal class WebSocketServerSecretProvider : CrestronLocalSecretsProvider + { + public WebSocketServerSecretProvider(string key) + : base(key) + { + Key = key; + } + } + + public class WebSocketServerSecret : ISecret + { + public ISecretProvider Provider { get; private set; } + + public string Key { get; private set; } + + public object Value { get; private set; } + + public WebSocketServerSecret(string key, object value, ISecretProvider provider) + { + Key = key; + Value = JsonConvert.SerializeObject(value); + Provider = provider; + } + + public ServerTokenSecrets DeserializeSecret() + { + return JsonConvert.DeserializeObject(Value.ToString()); + } + } + + +} diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index ddc60c09..5955faaa 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -261,6 +261,7 @@ namespace PepperDash.Essentials _ = new DeviceFactory(); _ = new ProcessorExtensionDeviceFactory(); + _ = new MobileControl.MobileControlFactory(); Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration"); @@ -469,26 +470,34 @@ namespace PepperDash.Essentials /// Reads all rooms from config and adds them to DeviceManager /// public void LoadRooms() - { + { if (ConfigReader.ConfigObject.Rooms == null) { Debug.LogMessage(LogEventLevel.Information, "Notice: Configuration contains no rooms - Is this intentional? This may be a valid configuration."); return; } - foreach (var roomConfig in ConfigReader.ConfigObject.Rooms) + foreach (var roomConfig in ConfigReader.ConfigObject.Rooms) { - var room = Core.DeviceFactory.GetDevice(roomConfig); - - DeviceManager.AddDevice(room); - if (room is ICustomMobileControl) + try { - continue; + var room = Core.DeviceFactory.GetDevice(roomConfig); + + DeviceManager.AddDevice(room); + if (room is ICustomMobileControl) + { + continue; + } + } catch (Exception ex) + { + Debug.LogMessage(ex, "Exception loading room {roomKey}:{roomType}", null, roomConfig.Key, roomConfig.Type); + continue; } } Debug.LogMessage(LogEventLevel.Information, "All Rooms Loaded."); + } /// diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index ec55aaa5..544c4a43 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -54,5 +54,7 @@ + + \ No newline at end of file From 7318dbb04eb39f30604b44ae9c9b1447c1259cf2 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 24 Mar 2025 22:39:52 -0500 Subject: [PATCH 09/26] ci(force-patch): add skip package check prop --- .github/workflows/EssentialsPlugins-builds-4-series-caller.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From af0855cea38a2745be4498fa1a7f54b4b1afd5a8 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 24 Mar 2025 23:36:17 -0500 Subject: [PATCH 10/26] build(force-patch): publish mc messenger package --- src/Directory.Build.props | 6 +++--- .../PepperDash.Essentials.MobileControl.Messengers.csproj | 3 +-- .../PepperDash.Essentials.MobileControl.csproj | 1 - 3 files changed, 4 insertions(+), 6 deletions(-) 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/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index fb9816ed..83dc2c17 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -9,13 +9,12 @@ mobile-control-messengers Copyright © 2024 bin\$(Configuration)\ - false + true true $(Version) false PepperDash Technology PepperDash.Essentials.Plugin.MobileControl.Messengers - https://github.com/PepperDash/Essentials crestron 4series diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index 054676e7..b43a8221 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -19,7 +19,6 @@ bin\$(Configuration)\ PepperDash Technologies PepperDash.Essentials.4Series.Plugin.MobileControl - https://github.com/PepperDash/Essentials crestron 4series From fe2cd573e55b37ef00e1f21e79b8017a7c30e51c Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 00:10:22 -0500 Subject: [PATCH 11/26] feat: remove DisplayBase from Core --- .../DeviceTypeInterfaces/IDisplay.cs | 8 + .../Devices/FIND HOMES Interfaces.cs | 26 +- .../Display/BasicIrDisplay.cs | 229 ------------- .../Display/DisplayBase.cs | 320 ------------------ ...lsHuddleSpaceFusionSystemControllerBase.cs | 17 +- .../PepperDash.Essentials.Core.csproj | 4 + .../Displays/DisplayBase.cs | 8 +- ...epperDash.Essentials.Devices.Common.csproj | 2 +- .../DisplayBaseMessenger.cs | 0 .../IChannelMessenger.cs | 0 .../IColorMessenger.cs | 0 .../IDPadMessenger.cs | 0 .../IDvrMessenger.cs | 0 .../IHasPowerMessenger.cs | 0 .../INumericMessenger.cs | 0 .../ISetTopBoxControlsMessenger.cs | 0 .../ITransportMessenger.cs | 0 .../CoreDisplayBaseMessenger.cs | 61 ---- .../CoreTwoWayDisplayBaseMessenger.cs | 91 ----- ...Essentials.MobileControl.Messengers.csproj | 1 + .../MobileControlSystemController.cs | 41 +-- ...PepperDash.Essentials.MobileControl.csproj | 2 + 22 files changed, 39 insertions(+), 771 deletions(-) create mode 100644 src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IDisplay.cs delete mode 100644 src/PepperDash.Essentials.Core/Display/BasicIrDisplay.cs delete mode 100644 src/PepperDash.Essentials.Core/Display/DisplayBase.cs rename src/PepperDash.Essentials.MobileControl.Messengers/{DeviceTypeExtenstions => DeviceTypeExtensions}/DisplayBaseMessenger.cs (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/{DeviceTypeExtenstions => DeviceTypeExtensions}/IChannelMessenger.cs (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/{DeviceTypeExtenstions => DeviceTypeExtensions}/IColorMessenger.cs (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/{DeviceTypeExtenstions => DeviceTypeExtensions}/IDPadMessenger.cs (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/{DeviceTypeExtenstions => DeviceTypeExtensions}/IDvrMessenger.cs (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/{DeviceTypeExtenstions => DeviceTypeExtensions}/IHasPowerMessenger.cs (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/{DeviceTypeExtenstions => DeviceTypeExtensions}/INumericMessenger.cs (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/{DeviceTypeExtenstions => DeviceTypeExtensions}/ISetTopBoxControlsMessenger.cs (100%) rename src/PepperDash.Essentials.MobileControl.Messengers/{DeviceTypeExtenstions => DeviceTypeExtensions}/ITransportMessenger.cs (100%) delete mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/CoreDisplayBaseMessenger.cs delete mode 100644 src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CoreTwoWayDisplayBaseMessenger.cs 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/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/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..ab04a0b0 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; @@ -1245,7 +1246,7 @@ namespace PepperDash.Essentials.Core.Fusion } //else - if (dev is DisplayBase) + if (dev is IDisplay) { attrNum = attrNum + displayNum; if (attrNum > JoinMap.DisplayOnlineStart.JoinSpan) @@ -1289,13 +1290,13 @@ 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; } @@ -1304,7 +1305,7 @@ namespace PepperDash.Essentials.Core.Fusion { return; } - var defaultDisplay = hasDefaultDisplay.DefaultDisplay as DisplayBase; + var defaultDisplay = hasDefaultDisplay.DefaultDisplay as IDisplay; if (defaultDisplay == null) { Debug.LogMessage(LogEventLevel.Debug, this, "Cannot link null display to Fusion because default display is null"); @@ -1370,8 +1371,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,7 +1387,7 @@ 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); diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index 778d471e..fecf88f9 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -6,6 +6,7 @@ net472 true + true bin\$(Configuration)\ PepperDash_Essentials_Core PepperDash.Essentials.Core @@ -31,4 +32,7 @@ + + + \ No newline at end of file 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/PepperDash.Essentials.Devices.Common.csproj b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj index a58861a5..5f8e6707 100644 --- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj +++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj @@ -9,7 +9,7 @@ bin\$(Configuration)\ Essentials Devices Common PepperDash.Essentials.Devices.Common - True + true PepperDash Essentials Devices Common PepperDash.Essentials.Devices.Common 2.0.0-local diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/DisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/DisplayBaseMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IChannelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IChannelMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IColorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IColorMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDPadMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDPadMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDvrMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IDvrMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IHasPowerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/IHasPowerMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/INumericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/INumericMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ISetTopBoxControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ISetTopBoxControlsMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ITransportMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs similarity index 100% rename from src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/ITransportMessenger.cs rename to src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/CoreDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/CoreDisplayBaseMessenger.cs deleted file mode 100644 index bf782710..00000000 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtenstions/CoreDisplayBaseMessenger.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Newtonsoft.Json.Linq; -using PepperDash.Core; -using PepperDash.Essentials.AppServer; -using PepperDash.Essentials.AppServer.Messengers; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System; -using System.Linq; -using DisplayBase = PepperDash.Essentials.Core.DisplayBase; - -namespace PepperDash.Essentials.Room.MobileControl -{ - public class CoreDisplayBaseMessenger: MessengerBase - { - private readonly DisplayBase display; - - public CoreDisplayBaseMessenger(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) - { - Debug.Console(1, "No input named {0} found for device {1}", 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/Messengers/CoreTwoWayDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CoreTwoWayDisplayBaseMessenger.cs deleted file mode 100644 index 8bce8a4e..00000000 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CoreTwoWayDisplayBaseMessenger.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Newtonsoft.Json.Linq; -using PepperDash.Core; -using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; - -namespace PepperDash.Essentials.AppServer.Messengers -{ - public class CoreTwoWayDisplayBaseMessenger : MessengerBase - { - private readonly TwoWayDisplayBase _display; - - public CoreTwoWayDisplayBaseMessenger(string key, string messagePath, Device display) - : base(key, messagePath, display) - { - _display = display as TwoWayDisplayBase; - } - - #region Overrides of MessengerBase - - public void SendFullStatus() - { - var messageObj = new TwoWayDisplayBaseStateMessage - { - //PowerState = _display.PowerIsOnFeedback.BoolValue, - CurrentInput = _display.CurrentInputFeedback.StringValue - }; - - PostStatusMessage(messageObj); - } - -#if SERIES4 - protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif - { - base.RegisterActions(); - if (_display == null) - { - Debug.Console(0, this, $"Unable to register TwoWayDisplayBase messenger {Key}"); - return; - } - - AddAction("/fullStatus", (id, content) => SendFullStatus()); - - _display.PowerIsOnFeedback.OutputChange += PowerIsOnFeedbackOnOutputChange; - _display.CurrentInputFeedback.OutputChange += CurrentInputFeedbackOnOutputChange; - _display.IsCoolingDownFeedback.OutputChange += IsCoolingFeedbackOnOutputChange; - _display.IsWarmingUpFeedback.OutputChange += IsWarmingFeedbackOnOutputChange; - } - - private void CurrentInputFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) - { - PostStatusMessage(JToken.FromObject(new - { - currentInput = feedbackEventArgs.StringValue - })); - } - - - private void PowerIsOnFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) - { - PostStatusMessage(JToken.FromObject(new - { - powerState = feedbackEventArgs.BoolValue - }) - ); - } - - private void IsWarmingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) - { - PostStatusMessage(JToken.FromObject(new - { - isWarming = feedbackEventArgs.BoolValue - }) - ); - - } - - private void IsCoolingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) - { - PostStatusMessage(JToken.FromObject(new - { - isCooling = feedbackEventArgs.BoolValue - }) - ); - } - - #endregion - } -} diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index 83dc2c17..196f83bd 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -6,6 +6,7 @@ PepperDash.Essentials.AppServer net472 mobile-control-messengers + mobile-control-messengers mobile-control-messengers Copyright © 2024 bin\$(Configuration)\ diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 86d8f48d..2ff51a4d 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -500,19 +500,19 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is Core.DisplayBase) - { - Debug.Console(2, this, "Adding actions for device: {0}", device.Key); + //if (device is Core.DisplayBase) + //{ + // Debug.Console(2, this, "Adding actions for device: {0}", device.Key); - var dbMessenger = new CoreDisplayBaseMessenger( - $"{device.Key}-displayBase-{Key}", - $"/device/{device.Key}", - device as Core.DisplayBase - ); - AddDefaultDeviceMessenger(dbMessenger); + // var dbMessenger = new CoreDisplayBaseMessenger( + // $"{device.Key}-displayBase-{Key}", + // $"/device/{device.Key}", + // device as Core.DisplayBase + // ); + // AddDefaultDeviceMessenger(dbMessenger); - messengerAdded = true; - } + // messengerAdded = true; + //} if (device is TwoWayDisplayBase) { @@ -533,25 +533,6 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is Core.TwoWayDisplayBase) - { - var display = device as Core.TwoWayDisplayBase; - Debug.Console( - 2, - this, - "Adding TwoWayDisplayBase for device: {0}", - device.Key - ); - var twoWayDisplayMessenger = new CoreTwoWayDisplayBaseMessenger( - $"{device.Key}-twoWayDisplay-{Key}", - string.Format("/device/{0}", device.Key), - display - ); - AddDefaultDeviceMessenger(twoWayDisplayMessenger); - - messengerAdded = true; - } - if (device is IBasicVolumeWithFeedback) { var deviceKey = device.Key; diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index b43a8221..66818fcf 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -8,12 +8,14 @@ true false epi-essentials-mobile-control + epi-essentials-mobile-control PepperDash Technologies epi-essentials-mobile-control This software is a plugin designed to work as a part of PepperDash Essentials for Crestron control processors. This plugin allows for connection to a PepperDash Mobile Control server. Copyright 2020 4.0.0-local true + true $(Version) false bin\$(Configuration)\ From aebc694da7d7d101d08977cbf4e4d3ac0bb82a71 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 00:16:23 -0500 Subject: [PATCH 12/26] build(force-patch): use version in directory.build.props --- src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj | 1 - .../PepperDash.Essentials.Devices.Common.csproj | 1 - .../PepperDash.Essentials.MobileControl.csproj | 1 - src/PepperDash.Essentials/PepperDash.Essentials.csproj | 1 - 4 files changed, 4 deletions(-) diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index fecf88f9..36c887ed 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -14,7 +14,6 @@ PepperDash.Essentials.Core $(Version) false - 2.0.0-local full 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 5f8e6707..57d0b67c 100644 --- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj +++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj @@ -12,7 +12,6 @@ true PepperDash Essentials Devices Common PepperDash.Essentials.Devices.Common - 2.0.0-local $(Version) false diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index 66818fcf..4971d765 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -13,7 +13,6 @@ epi-essentials-mobile-control This software is a plugin designed to work as a part of PepperDash Essentials for Crestron control processors. This plugin allows for connection to a PepperDash Mobile Control server. Copyright 2020 - 4.0.0-local true true $(Version) diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index 544c4a43..660daaa4 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -11,7 +11,6 @@ bin\$(Configuration)\ PepperDash Essentials PepperDashEssentials - 2.0.0-local $(Version) false From 8316ee22b6e910884349b7642f3471edc35253e5 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 00:20:56 -0500 Subject: [PATCH 13/26] build(force-patch): change names of MC packages In order to prevent eventual version collision and confusion, and to allow for deprecation of existing packages, the names of the MC packages are now reflective of the fact that they are no longer pluginsbuild(force-patch): change names of MC packages In order to prevent eventual version collision and confusion, and to allow for deprecation of existing packages, the names of the MC packages are now reflective of the fact that they are no longer plugins. --- .../PepperDash.Essentials.MobileControl.Messengers.csproj | 2 +- .../PepperDash.Essentials.MobileControl.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index 196f83bd..111d4c2c 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -15,7 +15,7 @@ $(Version) false PepperDash Technology - PepperDash.Essentials.Plugin.MobileControl.Messengers + PepperDash.Essentials.MobileControl.Messengers crestron 4series diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index 4971d765..5fda3af7 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -19,7 +19,7 @@ false bin\$(Configuration)\ PepperDash Technologies - PepperDash.Essentials.4Series.Plugin.MobileControl + PepperDash.Essentials.MobileControl crestron 4series From f6f731b4709862b7193cc425c8e1d77606c37c12 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 10:21:17 -0500 Subject: [PATCH 14/26] chore: remove 4SERIES compiler directive --- .../Displays/MockDisplay.cs | 2 +- .../DeviceTypeExtensions/IChannelMessenger.cs | 4 +- .../DeviceTypeExtensions/IDPadMessenger.cs | 4 +- .../DeviceTypeExtensions/IDvrMessenger.cs | 4 +- .../DeviceTypeExtensions/INumericMessenger.cs | 4 +- .../ISetTopBoxControlsMessenger.cs | 4 +- .../ITransportMessenger.cs | 4 +- .../Messengers/AudioCodecBaseMessenger.cs | 5 +- .../Messengers/CameraBaseMessenger.cs | 4 -- .../Messengers/DevicePresetsModelMessenger.cs | 5 +- .../Messengers/DeviceVolumeMessenger.cs | 5 +- .../Messengers/GenericMessenger.cs | 4 -- .../IHasScheduleAwarenessMessenger.cs | 4 -- .../Messengers/IRunRouteActionMessenger.cs | 4 -- .../Messengers/ISwitchedOutputMessenger.cs | 4 -- .../Messengers/LightingBaseMessenger.cs | 4 -- .../Messengers/MessengerBase.cs | 20 +------ .../Messengers/RoomEventScheduleMessenger.cs | 4 -- .../Messengers/SIMPLAtcMessenger.cs | 4 -- .../Messengers/SIMPLCameraMessenger.cs | 10 +--- .../Messengers/SIMPLDirectRouteMessenger.cs | 4 -- .../Messengers/SIMPLRouteMessenger.cs | 10 +--- .../Messengers/SIMPLVtcMessenger.cs | 4 -- .../Messengers/ShadeBaseMessenger.cs | 4 -- .../Messengers/SystemMonitorMessenger.cs | 4 -- .../Messengers/TwoWayDisplayBaseMessenger.cs | 7 +-- .../Messengers/VideoCodecBaseMessenger.cs | 4 -- .../MobileControlMessage.cs | 5 -- .../MobileControlConfig.cs | 20 +------ .../MobileControlSystemController.cs | 52 ++++++------------- ...PepperDash.Essentials.MobileControl.csproj | 5 +- .../MobileControlEssentialsRoomBridge.cs | 46 ++++++---------- .../MobileControlSIMPLRoomBridge.cs | 5 +- .../TransmitMessage.cs | 3 +- 34 files changed, 53 insertions(+), 223 deletions(-) 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.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs index 33d5ecd7..d02ecbf3 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs @@ -1,9 +1,7 @@ using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -#if SERIES4 -#endif + namespace PepperDash.Essentials.Room.MobileControl { public class IChannelMessenger:MessengerBase diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs index 6f72856c..000f9185 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs @@ -1,9 +1,7 @@ using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -#if SERIES4 -#endif + namespace PepperDash.Essentials.Room.MobileControl { public class IDPadMessenger:MessengerBase diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs index 4692aaf0..b722bd73 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs @@ -1,9 +1,7 @@ using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -#if SERIES4 -#endif + namespace PepperDash.Essentials.Room.MobileControl { public class IDvrMessenger: MessengerBase diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs index dc3290c9..6f601013 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs @@ -1,9 +1,7 @@ using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -#if SERIES4 -#endif + namespace PepperDash.Essentials.Room.MobileControl { public class INumericKeypadMessenger:MessengerBase diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs index 9b88b035..c47b5f47 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs @@ -1,9 +1,7 @@ using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -#if SERIES4 -#endif + namespace PepperDash.Essentials.Room.MobileControl { public class ISetTopBoxControlsMessenger:MessengerBase diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs index bc2f770e..cf34b490 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs @@ -1,9 +1,7 @@ using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -#if SERIES4 -#endif + namespace PepperDash.Essentials.Room.MobileControl { public class ITransportMessenger:MessengerBase diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs index 0472241d..2acb90b5 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs @@ -30,11 +30,8 @@ namespace PepperDash.Essentials.AppServer.Messengers codec.CallStatusChange += Codec_CallStatusChange; } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif + { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs index 1a8f0706..404dbf6c 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs @@ -46,11 +46,7 @@ namespace PepperDash.Essentials.AppServer.Messengers ); } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs index 36c6fd62..ee4aac53 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs @@ -40,11 +40,8 @@ namespace PepperDash.Essentials.AppServer.Messengers #region Overrides of MessengerBase -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif + { AddAction("/presets/fullStatus", (id, content) => { this.LogInformation("getting full status for client {id}", id); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs index aeeb3af0..6531424e 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs @@ -47,11 +47,8 @@ namespace PepperDash.Essentials.AppServer.Messengers #region Overrides of MessengerBase -#if SERIES4 + protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { AddAction("/fullStatus", (id, content) => SendStatus()); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs index 2a52db13..640df31a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs @@ -10,11 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs index 056f2f22..550f1045 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs @@ -20,11 +20,7 @@ namespace PepperDash.Essentials.AppServer.Messengers ScheduleSource.CodecSchedule.MeetingEventChange += new EventHandler(CodecSchedule_MeetingEventChange); } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { AddAction("/schedule/fullStatus", (id, content) => SendFullScheduleObject()); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs index 88d0a5bf..2579f056 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs @@ -31,11 +31,7 @@ namespace PepperDash.Essentials.AppServer.Messengers SendRoutingFullMessageObject(); } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { AddAction("/fullStatus", (id, content) => SendRoutingFullMessageObject()); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs index 6d3bceb6..9b6f21d7 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs @@ -21,11 +21,7 @@ namespace PepperDash.Essentials.AppServer.Messengers this.device = device; } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs index 4058c3a7..3f8e36e5 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs @@ -30,11 +30,7 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(state); } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index e73a4f0a..af9fbe43 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -13,11 +13,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// Provides a messaging bridge /// -#if SERIES4 public abstract class MessengerBase : EssentialsDevice, IMobileControlMessenger -#else - public abstract class MessengerBase: EssentialsDevice -#endif { protected IKeyName _device; @@ -30,11 +26,8 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// /// -#if SERIES4 + public IMobileControl AppServerController { get; private set; } -#else - public MobileControlSystemController AppServerController { get; private set; } -#endif public string MessagePath { get; private set; } @@ -76,11 +69,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// Registers this messenger with appserver controller /// /// -#if SERIES4 public void RegisterWithAppServer(IMobileControl appServerController) -#else - public void RegisterWithAppServer(MobileControlSystemController appServerController) -#endif { AppServerController = appServerController ?? throw new ArgumentNullException("appServerController"); @@ -133,11 +122,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// Implemented in extending classes. Wire up API calls and feedback here /// /// -#if SERIES4 protected virtual void RegisterActions() -#else - protected virtual void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { } @@ -174,7 +159,6 @@ namespace PepperDash.Essentials.AppServer.Messengers } } -#if SERIES4 protected void PostStatusMessage(string type, DeviceStateMessageBase deviceState, string clientId = null) { try @@ -195,7 +179,7 @@ namespace PepperDash.Essentials.AppServer.Messengers Debug.LogMessage(ex, "Exception posting status message", this); } } -#endif + protected void PostStatusMessage(JToken content, string type = "", string clientId = null) { try diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs index a32eb5a6..c316d13e 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs @@ -21,11 +21,7 @@ namespace PepperDash.Essentials.AppServer.Messengers #region Overrides of MessengerBase -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { AddAction("/saveScheduledEvents", (id, content) => SaveScheduledEvents(content.ToObject>())); AddAction("/status", (id, content) => diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs index 3f2ab694..1903b25f 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs @@ -57,11 +57,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// /// -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { //EISC.SetStringSigAction(SCurrentDialString, s => PostStatusMessage(new { currentDialString = s })); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs index 42111467..de3f349d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs @@ -32,11 +32,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { AddAction("/fullStatus", (id, content) => SendCameraFullMessageObject()); @@ -84,11 +80,7 @@ namespace PepperDash.Essentials.AppServer.Messengers cameraAction(state.Value.Equals("true", StringComparison.InvariantCultureIgnoreCase)); } -#if SERIES4 - public void CustomUnregsiterWithAppServer(IMobileControl appServerController) -#else - public void CustomUnregsiterWithAppServer(MobileControlSystemController appServerController) -#endif + public void CustomUnregisterWithAppServer(IMobileControl appServerController) { appServerController.RemoveAction(MessagePath + "/fullStatus"); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs index 46899cd1..cf95b753 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs @@ -26,11 +26,7 @@ namespace PepperDash.Essentials.AppServer.Messengers #region Overrides of MessengerBase -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController controller) -#endif { Debug.Console(2, "********** Direct Route Messenger CustomRegisterWithAppServer **********"); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs index ccdbb279..c1f6d78e 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs @@ -30,11 +30,7 @@ namespace PepperDash.Essentials.AppServer.Messengers _eisc.SetStringSigAction(_joinStart + StringJoin.CurrentSource, SendRoutingFullMessageObject); } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { AddAction("/fullStatus", (id, content) => SendRoutingFullMessageObject(_eisc.GetString(_joinStart + StringJoin.CurrentSource))); @@ -47,11 +43,7 @@ namespace PepperDash.Essentials.AppServer.Messengers }); } -#if SERIES4 - public void CustomUnregsiterWithAppServer(IMobileControl appServerController) -#else - public void CustomUnregsiterWithAppServer(MobileControlSystemController appServerController) -#endif + public void CustomUnregisterWithAppServer(IMobileControl appServerController) { appServerController.RemoveAction(MessagePath + "/fullStatus"); appServerController.RemoveAction(MessagePath + "/source"); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs index a421e068..e88acb12 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs @@ -43,11 +43,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// /// -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { _eisc.SetStringSigAction(JoinMap.HookState.JoinNumber, s => { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs index e004153b..7abf0fca 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs @@ -16,11 +16,7 @@ namespace PepperDash.Essentials.AppServer.Messengers device = shades; } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs index 92ffd638..9c13c569 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs @@ -83,11 +83,7 @@ namespace PepperDash.Essentials.AppServer.Messengers )); } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { AddAction("/fullStatus", (id, content) => SendFullStatusMessage()); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs index baf970db..ddd7e7ba 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs @@ -1,8 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using TwoWayDisplayBase = PepperDash.Essentials.Devices.Common.Displays.TwoWayDisplayBase; +using PepperDash.Essentials.Devices.Common.Displays; namespace PepperDash.Essentials.AppServer.Messengers { @@ -33,11 +32,7 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(messageObj); } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { base.RegisterActions(); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs index fecbc688..fab0fe3b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs @@ -152,11 +152,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// Called from base's RegisterWithAppServer method /// /// -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { try { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs index 5e284df2..6e92d9da 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/MobileControlMessage.cs @@ -4,12 +4,7 @@ using PepperDash.Essentials.Core.DeviceTypeInterfaces; namespace PepperDash.Essentials.AppServer.Messengers { - -#if SERIES4 public class MobileControlMessage : IMobileControlMessage -#else - public class MobileControlMessage -#endif { [JsonProperty("type")] public string Type { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs index f767830a..4493e288 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs @@ -16,30 +16,14 @@ namespace PepperDash.Essentials [JsonProperty("clientAppUrl")] public string ClientAppUrl { get; set; } -#if SERIES4 [JsonProperty("directServer")] public MobileControlDirectServerPropertiesConfig DirectServer { get; set; } [JsonProperty("applicationConfig")] - public MobileControlApplicationConfig ApplicationConfig { get; set; } + public MobileControlApplicationConfig ApplicationConfig { get; set; } = null; [JsonProperty("enableApiServer")] - public bool EnableApiServer { get; set; } -#endif - - [JsonProperty("roomBridges")] - [Obsolete("No longer necessary")] - public List RoomBridges { get; set; } - - public MobileControlConfig() - { - RoomBridges = new List(); - -#if SERIES4 - EnableApiServer = true; // default to true - ApplicationConfig = null; -#endif - } + public bool EnableApiServer { get; set; } = true; } public class MobileControlDirectServerPropertiesConfig diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 2ff51a4d..f7356f10 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -28,6 +28,7 @@ using PepperDash.Essentials.Core.Shades; using PepperDash.Essentials.Core.Web; using PepperDash.Essentials.Devices.Common.AudioCodec; using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Displays; using PepperDash.Essentials.Devices.Common.SoftCodec; using PepperDash.Essentials.Devices.Common.VideoCodec; using PepperDash.Essentials.Room.MobileControl; @@ -35,10 +36,6 @@ using PepperDash.Essentials.Services; using PepperDash.Essentials.WebApiHandlers; using Serilog.Events; using WebSocketSharp; -using DisplayBase = PepperDash.Essentials.Devices.Common.Displays.DisplayBase; -using TwoWayDisplayBase = PepperDash.Essentials.Devices.Common.Displays.TwoWayDisplayBase; -#if SERIES4 -#endif namespace PepperDash.Essentials { @@ -59,16 +56,11 @@ namespace PepperDash.Essentials private readonly List _roomBridges = new List(); -#if SERIES4 private readonly Dictionary _messengers = new Dictionary(); private readonly Dictionary _defaultMessengers = new Dictionary(); -#else - private readonly Dictionary _deviceMessengers = - new Dictionary(); -#endif private readonly GenericQueue _transmitToServerQueue; @@ -81,11 +73,10 @@ namespace PepperDash.Essentials public List RoomBridges => _roomBridges; -#if SERIES4 private readonly MobileControlWebsocketServer _directServer; public MobileControlWebsocketServer DirectServer => _directServer; -#endif + private readonly CCriticalSection _wsCriticalSection = new CCriticalSection(); public string SystemUrl; //set only from SIMPL Bridge! @@ -191,7 +182,6 @@ namespace PepperDash.Essentials 25 ); -#if SERIES4 if (Config.DirectServer != null && Config.DirectServer.EnableDirectServer) { _directServer = new MobileControlWebsocketServer( @@ -207,7 +197,6 @@ namespace PepperDash.Essentials 25 ); } -#endif Host = config.ServerUrl; if (!Host.StartsWith("http")) @@ -514,9 +503,8 @@ namespace PepperDash.Essentials // messengerAdded = true; //} - if (device is TwoWayDisplayBase) - { - var display = device as TwoWayDisplayBase; + if (device is TwoWayDisplayBase twoWayDisplay) + { Debug.Console( 2, this, @@ -526,7 +514,7 @@ namespace PepperDash.Essentials var twoWayDisplayMessenger = new TwoWayDisplayBaseMessenger( $"{device.Key}-twoWayDisplay-{Key}", string.Format("/device/{0}", device.Key), - display + twoWayDisplay ); AddDefaultDeviceMessenger(twoWayDisplayMessenger); @@ -1281,11 +1269,7 @@ namespace PepperDash.Essentials return _messengers.ContainsKey(key); } -#if SERIES4 public void AddDeviceMessenger(IMobileControlMessenger messenger) -#else - public void AddDeviceMessenger(MessengerBase messenger) -#endif { if (_messengers.ContainsKey(messenger.Key)) { @@ -1815,10 +1799,9 @@ namespace PepperDash.Essentials var conn = _wsClient2 == null ? "No client" : (_wsClient2.IsAlive ? "Yes" : "No"); var secSinceLastAck = DateTime.Now - _lastAckMessage; -#if SERIES4 + if (Config.EnableApiServer) { -#endif CrestronConsole.ConsoleCommandResponse( @"Mobile Control Edge Server API Information: @@ -1837,7 +1820,6 @@ namespace PepperDash.Essentials conn, secSinceLastAck.Seconds ); -#if SERIES4 } else { @@ -1916,7 +1898,6 @@ Mobile Control Direct Server Infromation: Not Enabled in Config." ); } -#endif } /// @@ -1924,7 +1905,7 @@ Mobile Control Direct Server Infromation: /// public void RegisterSystemToServer() { -#if SERIES4 + if (!Config.EnableApiServer) { Debug.Console( @@ -1934,7 +1915,7 @@ Mobile Control Direct Server Infromation: ); return; } -#endif + var result = CreateWebsocket(); if (!result) @@ -2177,10 +2158,10 @@ Mobile Control Direct Server Infromation: var essentialsVersion = Global.AssemblyVersion; confObject.Info.RuntimeInfo.AssemblyVersion = essentialsVersion; -//#if DEBUG + // // Set for local testing // confObject.RuntimeInfo.PluginVersion = "4.0.0-localBuild"; -//#else + // Populate the plugin version var pluginVersion = Assembly .GetExecutingAssembly() @@ -2197,7 +2178,6 @@ Mobile Control Direct Server Infromation: confObject.RuntimeInfo.PepperDashCoreVersion = PluginLoader.PepperDashCoreAssembly.Version; confObject.RuntimeInfo.EssentialsPlugins = PluginLoader.EssentialsPluginAssemblies; } -//#endif return confObject; } @@ -2218,12 +2198,12 @@ Mobile Control Direct Server Infromation: /// public void SendMessageObject(IMobileControlMessage o) { -#if SERIES4 + if (Config.EnableApiServer) { -#endif + _transmitToServerQueue.Enqueue(new TransmitMessage(o, _wsClient2)); -#if SERIES4 + } if ( @@ -2234,10 +2214,10 @@ Mobile Control Direct Server Infromation: { _transmitToClientsQueue.Enqueue(new MessageToClients(o, _directServer)); } -#endif + } -#if SERIES4 + public void SendMessageObjectToDirectClient(object o) { if ( @@ -2249,7 +2229,7 @@ Mobile Control Direct Server Infromation: _transmitToClientsQueue.Enqueue(new MessageToClients(o, _directServer)); } } -#endif + /// /// Disconnects the Websocket Client and stops the heartbeat timer diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index 5fda3af7..ca694e96 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -1,7 +1,4 @@ - - - ProgramLibrary - + PepperDash.Essentials net472 diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs index a64d7e43..ba6a3b0b 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs @@ -1,36 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; +using PepperDash.Essentials.AppServer; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using PepperDash.Essentials.Room.MobileControl; -using PepperDash.Essentials.Room.Config; -using PepperDash.Essentials.Devices.Common.VideoCodec; -using PepperDash.Essentials.Devices.Common.AudioCodec; -using PepperDash.Essentials.Devices.Common.Cameras; - -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using PepperDash.Essentials.Devices.Common.Room; -using IShades = PepperDash.Essentials.Core.Shades.IShades; -using ShadeBase = PepperDash.Essentials.Devices.Common.Shades.ShadeBase; -using PepperDash.Essentials.Devices.Common.TouchPanel; -using Crestron.SimplSharp; -using Volume = PepperDash.Essentials.Room.MobileControl.Volume; using PepperDash.Essentials.Core.CrestronIO; +using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.Lighting; using PepperDash.Essentials.Core.Shades; -using PepperDash.Core.Logging; - - - -#if SERIES4 -using PepperDash.Essentials.AppServer; -#endif +using PepperDash.Essentials.Devices.Common.AudioCodec; +using PepperDash.Essentials.Devices.Common.Cameras; +using PepperDash.Essentials.Devices.Common.Room; +using PepperDash.Essentials.Devices.Common.VideoCodec; +using PepperDash.Essentials.Room.Config; +using System; +using System.Collections.Generic; +using System.Linq; +using IShades = PepperDash.Essentials.Core.Shades.IShades; +using ShadeBase = PepperDash.Essentials.Devices.Common.Shades.ShadeBase; +using Volume = PepperDash.Essentials.Room.MobileControl.Volume; namespace PepperDash.Essentials { @@ -66,11 +57,8 @@ namespace PepperDash.Essentials AddPreActivationAction(GetRoom); } -#if SERIES4 + protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif { // we add actions to the messaging system with a path, and a related action. Custom action // content objects can be handled in the controller's LineReceived method - and perhaps other diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs index a617200c..8febf414 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs @@ -175,11 +175,8 @@ namespace PepperDash.Essentials.Room.MobileControl ConfigIsLoaded = true; } -#if SERIES4 protected override void RegisterActions() -#else - protected override void CustomRegisterWithAppServer(MobileControlSystemController appServerController) -#endif + { SetupFunctions(); SetupFeedbacks(); diff --git a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs index 912d6af1..76c5f1b3 100644 --- a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs +++ b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs @@ -71,7 +71,7 @@ namespace PepperDash.Essentials } -#if SERIES4 + public class MessageToClients : IQueueMessage { private readonly MobileControlWebsocketServer _server; @@ -146,5 +146,4 @@ namespace PepperDash.Essentials #endregion } -#endif } \ No newline at end of file From 4d98191fa70430dad14fd0d460b4c9bf9cbfba26 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 21:53:41 -0500 Subject: [PATCH 15/26] chore: remove obsolete log methods --- .../MobileControlDeviceFactory.cs | 14 +- .../MobileControlSystemController.cs | 662 ++++++------------ .../MobileControlSIMPLRoomBridge.cs | 3 +- .../Touchpanel/ITswAppControlMessenger.cs | 3 +- .../Touchpanel/ITswZoomControlMessenger.cs | 3 +- .../TransmitMessage.cs | 103 ++- .../WebApiHandlers/UiClientHandler.cs | 3 +- .../MobileControlWebsocketServer.cs | 99 +-- 8 files changed, 309 insertions(+), 581 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs index 0d589c9f..6ccbb382 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs @@ -1,7 +1,9 @@ using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Room.MobileControl; +using Serilog.Events; using System; using System.Collections.Generic; using System.Linq; @@ -50,13 +52,10 @@ namespace PepperDash.Essentials if (parent == null) { - Debug.Console(0, bridge, "ERROR: Cannot connect bridge. System controller not present"); + bridge.LogInformation("ERROR: Cannot connect bridge. System controller not present"); return; } - Debug.Console(0, bridge, "Linking to parent controller"); - - /*bridge.AddParent(parent); - parent.AddBridge(bridge);*/ + bridge.LogInformation("Linking to parent controller"); parent.AddDeviceMessenger(bridge); }); @@ -70,8 +69,7 @@ namespace PepperDash.Essentials if (mobileControlList.Count > 1) { - Debug.Console(0, Debug.ErrorLogLevel.Warning, - "Multiple instances of Mobile Control Server found."); + Debug.LogMessage(LogEventLevel.Warning, "Multiple instances of Mobile Control Server found."); return null; } @@ -80,7 +78,7 @@ namespace PepperDash.Essentials return mobileControlList[0]; } - Debug.Console(0, Debug.ErrorLogLevel.Notice, "Mobile Control not enabled for this system"); + Debug.LogMessage(LogEventLevel.Warning, "Mobile Control not enabled for this system"); return null; } } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index f7356f10..898fd10b 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -98,19 +98,13 @@ namespace PepperDash.Essentials return ConfigReader.ConfigObject.SystemUuid; } - Debug.Console( - 0, - this, - Debug.ErrorLogLevel.Notice, + this.LogWarning( "No system_url value defined in config. Checking for value from SIMPL Bridge." ); if (!string.IsNullOrEmpty(SystemUrl)) { - Debug.Console( - 0, - this, - Debug.ErrorLogLevel.Error, + this.LogError( "No system_url value defined in config or SIMPL Bridge. Unable to connect to Mobile Control." ); return string.Empty; @@ -206,9 +200,7 @@ namespace PepperDash.Essentials ApiService = new MobileControlApiService(Host); - Debug.Console( - 0, - this, + this.LogInformation( "Mobile UI controller initializing for server:{0}", config.ServerUrl ); @@ -250,15 +242,15 @@ namespace PepperDash.Essentials private void SetupDefaultRoomMessengers() { - Debug.LogMessage(LogEventLevel.Verbose, "Setting up room messengers", this); + this.LogVerbose("Setting up room messengers"); + foreach (var room in DeviceManager.AllDevices.OfType()) { - Debug.LogMessage( - LogEventLevel.Verbose, - "Setting up room messengers for room: {key}", - this, + this.LogVerbose( + "Setting up room messengers for room: {key}", room.Key ); + var messenger = new MobileControlEssentialsRoomBridge(room); messenger.AddParent(this); @@ -267,16 +259,14 @@ namespace PepperDash.Essentials AddDefaultDeviceMessenger(messenger); - Debug.LogMessage( - LogEventLevel.Verbose, - "Attempting to set up default room messengers for room: {0}", - this, + this.LogVerbose( + "Attempting to set up default room messengers for room: {0}", room.Key ); if (room is IRoomEventSchedule) { - Debug.LogMessage(LogEventLevel.Information, "Setting up event schedule messenger for room: {key}", this, room.Key); + this.LogInformation("Setting up event schedule messenger for room: {key}", room.Key); var scheduleMessenger = new RoomEventScheduleMessenger( $"{room.Key}-schedule-{Key}", @@ -289,7 +279,7 @@ namespace PepperDash.Essentials if (room is ITechPassword) { - Debug.LogMessage(LogEventLevel.Information, "Setting up tech password messenger for room: {key}", this, room.Key); + this.LogInformation("Setting up tech password messenger for room: {key}", room.Key); var techPasswordMessenger = new ITechPasswordMessenger( $"{room.Key}-techPassword-{Key}", @@ -302,7 +292,7 @@ namespace PepperDash.Essentials if (room is IShutdownPromptTimer) { - Debug.LogMessage(LogEventLevel.Information, "Setting up shutdown prompt timer messenger for room: {key}", this, room.Key); + this.LogInformation("Setting up shutdown prompt timer messenger for room: {key}", this, room.Key); var shutdownPromptTimerMessenger = new IShutdownPromptTimerMessenger( $"{room.Key}-shutdownPromptTimer-{Key}", @@ -315,7 +305,7 @@ namespace PepperDash.Essentials if (room is ILevelControls levelControls) { - Debug.LogMessage(LogEventLevel.Information, "Setting up level controls messenger for room: {key}", this, room.Key); + this.LogInformation("Setting up level controls messenger for room: {key}", this, room.Key); var levelControlsMessenger = new ILevelControlsMessenger( $"{room.Key}-levelControls-{Key}", @@ -334,77 +324,71 @@ namespace PepperDash.Essentials private void SetupDefaultDeviceMessengers() { bool messengerAdded = false; + var allDevices = DeviceManager.AllDevices.Where((d) => !(d is IEssentialsRoom)); - Debug.LogMessage( - LogEventLevel.Verbose, - "All Devices that aren't rooms count: {0}", - this, + + this.LogInformation( + "All Devices that aren't rooms count: {0}", allDevices?.Count() ); + var count = allDevices.Count(); + foreach (var device in allDevices) { try { - Debug.LogMessage( - LogEventLevel.Verbose, - "Attempting to set up device messengers for device: {0}", - this, + this.LogVerbose( + "Attempting to set up device messengers for {deviceKey}", device.Key ); + // StatusMonitorBase which is prop of ICommunicationMonitor is not a PepperDash.Core.Device, but is in the device array if (device is ICommunicationMonitor) { - Debug.LogMessage( - LogEventLevel.Verbose, - "Trying to cast to ICommunicationMonitor for device: {0}", - this, + this.LogVerbose( + "Checking if {deviceKey} implements ICommunicationMonitor", device.Key ); - var commMonitor = device as ICommunicationMonitor; - if (commMonitor == null) + + if (!(device is ICommunicationMonitor commMonitor)) { - Debug.LogMessage( - LogEventLevel.Debug, - "[Error] CommunicationMonitor cast is null for device: {0}. Skipping CommunicationMonitorMessenger", - this, + this.LogDebug( + "{deviceKey} does not implement ICommunicationMonitor. Skipping CommunicationMonitorMessenger", device.Key ); - Debug.LogMessage( - LogEventLevel.Debug, - "AllDevices Completed a device. Devices Left: {0}", - this, - --count - ); + + this.LogDebug("Created all messengers for {deviceKey}. Devices Left: {deviceCount}", device.Key, --count); + continue; } - Debug.LogMessage( - LogEventLevel.Debug, - "Adding CommunicationMonitorMessenger for device: {0}", - this, + + this.LogDebug( + "Adding CommunicationMonitorMessenger for {deviceKey}", device.Key ); + var commMessenger = new ICommunicationMonitorMessenger( $"{device.Key}-commMonitor-{Key}", string.Format("/device/{0}", device.Key), commMonitor ); + AddDefaultDeviceMessenger(commMessenger); + messengerAdded = true; } - if (device is CameraBase) + if (device is CameraBase cameraDevice) { - Debug.Console( - 2, - this, - "Adding CameraBaseMessenger for device: {0}", + this.LogVerbose( + "Adding CameraBaseMessenger for {deviceKey}", device.Key ); var cameraMessenger = new CameraBaseMessenger( $"{device.Key}-cameraBase-{Key}", - device as CameraBase, + cameraDevice, $"/device/{device.Key}" ); @@ -415,10 +399,8 @@ namespace PepperDash.Essentials if (device is BlueJeansPc) { - Debug.Console( - 2, - this, - "Adding IRunRouteActionMessnger for device: {0}", + this.LogVerbose( + "Adding IRunRouteActionMessnger for {deviceKey}", device.Key ); @@ -435,30 +417,16 @@ namespace PepperDash.Essentials if (device is ITvPresetsProvider) { - Debug.LogMessage( - LogEventLevel.Verbose, - "Trying to cast to ITvPresetsProvider for device: {0}", - this, + this.LogVerbose( + "Trying to cast to ITvPresetsProvider for {deviceKey}", device.Key ); var presetsDevice = device as ITvPresetsProvider; - if (presetsDevice.TvPresets == null) - { - Debug.Console( - 2, - this, - "TvPresets is null for device: '{0}'. Skipping DevicePresetsModelMessenger", - device.Key - ); - } - else - { - Debug.Console( - 2, - this, - "Adding ITvPresetsProvider for device: {0}", + + this.LogVerbose( + "Adding ITvPresetsProvider for {deviceKey}", device.Key ); @@ -472,11 +440,11 @@ namespace PepperDash.Essentials messengerAdded = true; } - } + if (device is DisplayBase) { - Debug.Console(2, this, "Adding actions for device: {0}", device.Key); + this.LogVerbose( "Adding actions for device: {0}", device.Key); var dbMessenger = new DisplayBaseMessenger( $"{device.Key}-displayBase-{Key}", @@ -489,26 +457,10 @@ namespace PepperDash.Essentials messengerAdded = true; } - //if (device is Core.DisplayBase) - //{ - // Debug.Console(2, this, "Adding actions for device: {0}", device.Key); - - // var dbMessenger = new CoreDisplayBaseMessenger( - // $"{device.Key}-displayBase-{Key}", - // $"/device/{device.Key}", - // device as Core.DisplayBase - // ); - // AddDefaultDeviceMessenger(dbMessenger); - - // messengerAdded = true; - //} - if (device is TwoWayDisplayBase twoWayDisplay) { - Debug.Console( - 2, - this, - "Adding TwoWayDisplayBase for device: {0}", + this.LogVerbose( + "Adding TwoWayDisplayBase for {deviceKey}", device.Key ); var twoWayDisplayMessenger = new TwoWayDisplayBaseMessenger( @@ -524,12 +476,11 @@ namespace PepperDash.Essentials if (device is IBasicVolumeWithFeedback) { var deviceKey = device.Key; - Debug.Console( - 2, - this, - "Adding IBasicVolumeControlWithFeedback for device: {0}", - deviceKey - ); + this.LogVerbose( + "Adding IBasicVolumeControlWithFeedback for {deviceKey}", + deviceKey + ); + var volControlDevice = device as IBasicVolumeWithFeedback; var messenger = new DeviceVolumeMessenger( $"{device.Key}-volume-{Key}", @@ -544,12 +495,12 @@ namespace PepperDash.Essentials if (device is ILightingScenes) { var deviceKey = device.Key; - Debug.Console( - 2, - this, - "Adding LightingBaseMessenger for device: {0}", + + this.LogVerbose( + "Adding LightingBaseMessenger for {deviceKey}", deviceKey ); + var lightingDevice = device as ILightingScenes; var messenger = new ILightingScenesMessenger( $"{device.Key}-lighting-{Key}", @@ -565,12 +516,12 @@ namespace PepperDash.Essentials { var deviceKey = device.Key; var shadeDevice = device as IShadesOpenCloseStop; - Debug.Console( - 2, - this, - "Adding ShadeBaseMessenger for device: {0}", + + this.LogVerbose( + "Adding ShadeBaseMessenger for {deviceKey}", deviceKey ); + var messenger = new IShadesOpenCloseStopMessenger( $"{device.Key}-shades-{Key}", shadeDevice, @@ -583,11 +534,8 @@ namespace PepperDash.Essentials if (device is VideoCodecBase codec) { - Debug.Console( - 2, - this, - $"Adding VideoCodecBaseMessenger for device: {codec.Key}" - ); + this.LogVerbose( + "Adding VideoCodecBaseMessenger for {deviceKey}", codec.Key ); var messenger = new VideoCodecBaseMessenger( $"{codec.Key}-videoCodec-{Key}", @@ -602,10 +550,8 @@ namespace PepperDash.Essentials if (device is AudioCodecBase audioCodec) { - Debug.Console( - 2, - this, - $"Adding AudioCodecBaseMessenger for device: {audioCodec.Key}" + this.LogVerbose( + "Adding AudioCodecBaseMessenger for {deviceKey}", audioCodec.Key ); var messenger = new AudioCodecBaseMessenger( @@ -621,13 +567,11 @@ namespace PepperDash.Essentials if (device is ISetTopBoxControls) { - Debug.Console( - 2, - this, - $"Adding ISetTopBoxControlMessenger for device: {device.Key}" + this.LogVerbose( + "Adding ISetTopBoxControlMessenger for {deviceKey}" ); - var dev = device as PepperDash.Core.Device; + var dev = device as Device; var messenger = new ISetTopBoxControlsMessenger( $"{device.Key}-stb-{Key}", @@ -642,10 +586,8 @@ namespace PepperDash.Essentials if (device is IChannel) { - Debug.Console( - 2, - this, - $"Adding IChannelMessenger for device: {device.Key}" + this.LogVerbose( + "Adding IChannelMessenger for {deviceKey}", device.Key ); var dev = device as PepperDash.Core.Device; @@ -663,7 +605,7 @@ namespace PepperDash.Essentials if (device is IColor) { - Debug.Console(2, this, $"Adding IColorMessenger for device: {device.Key}"); + this.LogVerbose("Adding IColorMessenger for {deviceKey}", device.Key); var dev = device as PepperDash.Core.Device; @@ -680,7 +622,7 @@ namespace PepperDash.Essentials if (device is IDPad) { - Debug.Console(2, this, $"Adding IDPadMessenger for device: {device.Key}"); + this.LogVerbose("Adding IDPadMessenger for {deviceKey}", device.Key); var dev = device as PepperDash.Core.Device; @@ -697,11 +639,7 @@ namespace PepperDash.Essentials if (device is INumericKeypad) { - Debug.Console( - 2, - this, - $"Adding INumericKeyapdMessenger for device: {device.Key}" - ); + this.LogVerbose("Adding INumericKeyapdMessenger for {deviceKey}", device.Key); var dev = device as PepperDash.Core.Device; @@ -718,11 +656,7 @@ namespace PepperDash.Essentials if (device is IHasPowerControl) { - Debug.Console( - 2, - this, - $"Adding IHasPowerControlMessenger for device: {device.Key}" - ); + this.LogVerbose("Adding IHasPowerControlMessenger for {deviceKey}", device.Key); var dev = device as PepperDash.Core.Device; @@ -740,10 +674,8 @@ namespace PepperDash.Essentials if (device is IHasPowerControlWithFeedback powerControl) { var deviceKey = device.Key; - Debug.Console( - 2, - this, - "Adding IHasPowerControlWithFeedbackMessenger for device: {0}", + this.LogVerbose( + "Adding IHasPowerControlWithFeedbackMessenger for {deviceKey}", deviceKey ); @@ -758,10 +690,8 @@ namespace PepperDash.Essentials if (device is ITransport) { - Debug.Console( - 2, - this, - $"Adding ITransportMessenger for device: {device.Key}" + this.LogVerbose( + "Adding ITransportMessenger for {deviceKey}", device.Key ); var dev = device as PepperDash.Core.Device; @@ -779,11 +709,7 @@ namespace PepperDash.Essentials if (device is IHasCurrentSourceInfoChange) { - Debug.Console( - 2, - this, - $"Adding IHasCurrentSourceInfoMessenger for device: {device.Key}" - ); + this.LogVerbose("Adding IHasCurrentSourceInfoMessenger for {deviceKey}", device.Key ); var messenger = new IHasCurrentSourceInfoMessenger( $"{device.Key}-currentSource-{Key}", @@ -798,10 +724,8 @@ namespace PepperDash.Essentials if (device is ISwitchedOutput) { - Debug.Console( - 2, - this, - $"Adding ISwitchedOutputMessenger for device: {device.Key}" + this.LogVerbose( + "Adding ISwitchedOutputMessenger for {deviceKey}", device.Key ); var messenger = new ISwitchedOutputMessenger( @@ -817,10 +741,7 @@ namespace PepperDash.Essentials if (device is IDeviceInfoProvider provider) { - Debug.Console( - 2, - this, - $"Adding IHasDeviceInfoMessenger for device: {device.Key}" + this.LogVerbose("Adding IHasDeviceInfoMessenger for {deviceKey}", device.Key ); var messenger = new DeviceInfoMessenger( @@ -836,10 +757,8 @@ namespace PepperDash.Essentials if (device is ILevelControls levelControls) { - Debug.Console( - 2, - this, - $"Adding LevelControlsMessenger for device: {device.Key}" + this.LogVerbose( + "Adding LevelControlsMessenger for {deviceKey}", device.Key ); var messenger = new ILevelControlsMessenger( @@ -855,41 +774,41 @@ namespace PepperDash.Essentials // This will work if TKey and TSelector are both string types. // Otherwise plugin device needs to instantiate ISelectableItemsMessenger and add it to the controller. - if (device is IHasInputs inputs) - { - Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + //if (device is IHasInputs inputs) + //{ + // this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); - var messenger = new ISelectableItemsMessenger( - $"{device.Key}-inputs-{Key}", - $"/device/{device.Key}", - inputs.Inputs, - "inputs" - ); + // var messenger = new ISelectableItemsMessenger( + // $"{device.Key}-inputs-{Key}", + // $"/device/{device.Key}", + // inputs.Inputs, + // "inputs" + // ); - AddDefaultDeviceMessenger(messenger); + // AddDefaultDeviceMessenger(messenger); - messengerAdded = true; - } + // messengerAdded = true; + //} - if (device is IHasInputs byteIntInputs) - { - Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + //if (device is IHasInputs byteIntInputs) + //{ + // this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); - var messenger = new ISelectableItemsMessenger( - $"{device.Key}-inputs-{Key}", - $"/device/{device.Key}", - byteIntInputs.Inputs, - "inputs" - ); + // var messenger = new ISelectableItemsMessenger( + // $"{device.Key}-inputs-{Key}", + // $"/device/{device.Key}", + // byteIntInputs.Inputs, + // "inputs" + // ); - AddDefaultDeviceMessenger(messenger); + // AddDefaultDeviceMessenger(messenger); - messengerAdded = true; - } + // messengerAdded = true; + //} if (device is IHasInputs stringInputs) { - Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); var messenger = new ISelectableItemsMessenger( $"{device.Key}-inputs-{Key}", @@ -905,7 +824,7 @@ namespace PepperDash.Essentials if (device is IHasInputs byteInputs) { - Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); var messenger = new ISelectableItemsMessenger( $"{device.Key}-inputs-{Key}", @@ -921,7 +840,7 @@ namespace PepperDash.Essentials if (device is IHasInputs intInputs) { - Debug.Console(2, this, $"Adding InputsMessenger for device: {device.Key}"); + this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); var messenger = new ISelectableItemsMessenger( $"{device.Key}-inputs-{Key}", @@ -938,10 +857,8 @@ namespace PepperDash.Essentials if (device is IMatrixRouting matrix) { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Verbose, - "Adding IMatrixRoutingMessenger for device: {key}", - this, + this.LogVerbose( + "Adding IMatrixRoutingMessenger for {deviceKey}", device.Key ); @@ -958,10 +875,8 @@ namespace PepperDash.Essentials if (device is ITemperatureSensor tempSensor) { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Verbose, - "Adding ITemperatureSensor for device: {key}", - this, + this.LogVerbose( + "Adding ITemperatureSensor for {deviceKey}", device.Key ); @@ -978,10 +893,8 @@ namespace PepperDash.Essentials if (device is IHumiditySensor humSensor) { - Debug.LogMessage( - Serilog.Events.LogEventLevel.Verbose, - "Adding IHumiditySensor for device: {key}", - this, + this.LogVerbose( + "Adding IHumiditySensor for {deviceKey}", device.Key ); @@ -998,10 +911,8 @@ namespace PepperDash.Essentials if (device is IEssentialsRoomCombiner roomCombiner) { - Debug.Console( - 2, - this, - $"Adding IEssentialsRoomCombinerMessenger for device: {device.Key}" + this.LogVerbose( + "Adding IEssentialsRoomCombinerMessenger for {deviceKey}", device.Key ); var messenger = new IEssentialsRoomCombinerMessenger( @@ -1014,12 +925,10 @@ namespace PepperDash.Essentials messengerAdded = true; } + if (device is IProjectorScreenLiftControl screenLiftControl) { - Debug.Console( - 2, - this, - $"Adding IProjectorScreenLiftControlMessenger for device: {device.Key}" + this.LogVerbose("Adding IProjectorScreenLiftControlMessenger for {deviceKey}", device.Key ); var messenger = new IProjectorScreenLiftControlMessenger( @@ -1035,10 +944,7 @@ namespace PepperDash.Essentials if (device is IDspPresets dspPresets) { - Debug.Console( - 2, - this, - $"Adding IDspPresetsMessenger for device: {device.Key}" + this.LogVerbose("Adding IDspPresetsMessenger for {deviceKey}", device.Key ); var messenger = new IDspPresetsMessenger( @@ -1052,39 +958,32 @@ namespace PepperDash.Essentials messengerAdded = true; } - Debug.LogMessage( - LogEventLevel.Verbose, - "Trying to cast to generic device for device: {key}", - this, - device.Key - ); + this.LogVerbose("Trying to cast to generic device for device: {key}", device.Key ); + if (device is EssentialsDevice) { var genericDevice = device as EssentialsDevice; + if (genericDevice == null || messengerAdded) { - Debug.LogMessage( - LogEventLevel.Verbose, - "Skipping GenericMessenger for device: {0}. Messenger Added: {1}. GenericDevice null: {2}", - this, + this.LogVerbose( + "Skipping GenericMessenger for {deviceKey}. Messenger(s) Added: {messengersAdded}.", device.Key, - messengerAdded, - genericDevice == null + messengerAdded ); - Debug.LogMessage( - LogEventLevel.Debug, - "AllDevices Completed a device. Devices Left: {0}", - this, + this.LogDebug( + "AllDevices Completed a device. Devices Left: {count}", --count ); continue; } - Debug.LogMessage( - LogEventLevel.Debug, - "Adding GenericMessenger for device: {0}", + + this.LogDebug( + "Adding GenericMessenger for {deviceKey}", this, genericDevice?.Key ); + AddDefaultDeviceMessenger( new GenericMessenger( genericDevice.Key + "-" + Key + "-generic", @@ -1095,29 +994,20 @@ namespace PepperDash.Essentials } else { - Debug.LogMessage( - LogEventLevel.Verbose, - "Not Essentials Device. Skipping GenericMessenger for device: {0}", - this, + this.LogVerbose( + "Not Essentials Device. Skipping GenericMessenger for {deviceKey}", device.Key ); } - Debug.LogMessage( - LogEventLevel.Debug, - "AllDevices Completed a device. Devices Left: {0}", - this, + this.LogDebug( + "AllDevices Completed a device. Devices Left: {count}", --count ); } + catch (Exception ex) { - Debug.LogMessage( - LogEventLevel.Verbose, - "[ERROR] setting up default device messengers: {0}", - this, - ex.Message - ); - Debug.LogMessage(ex, "[ERROR] setting up default device messengers", this); + this.LogException(ex, "Exception setting up default device messengers"); } } } @@ -1130,10 +1020,11 @@ namespace PepperDash.Essentials if (apiServer == null) { - Debug.Console(0, this, "No API Server available"); + this.LogWarning("No API Server available"); return; } + // TODO: Add routes for the rest of the MC console commands var routes = new List { new HttpCwsRoute($"device/{Key}/authorize") @@ -1204,31 +1095,28 @@ namespace PepperDash.Essentials s => { _disableReconnect = false; - Debug.Console( - 1, - this, - Debug.ErrorLogLevel.Notice, - "User command: {0}", - "mobileConnect" + + CrestronConsole.ConsoleCommandResponse( + $"Connecting to MC API server" ); - ConnectWebsocketClient(); + + ConnectWebsocketClient(); }, "mobileconnect", "Forces connect of websocket", ConsoleAccessLevelEnum.AccessOperator ); + CrestronConsole.AddNewConsoleCommand( s => { _disableReconnect = true; - Debug.Console( - 1, - this, - Debug.ErrorLogLevel.Notice, - "User command: {0}", - "mobileDisco" - ); + CleanUpWebsocketClient(); + + CrestronConsole.ConsoleCommandResponse( + $"Disonnected from MC API server" + ); }, "mobiledisco", "Disconnects websocket", @@ -1273,7 +1161,7 @@ namespace PepperDash.Essentials { if (_messengers.ContainsKey(messenger.Key)) { - Debug.Console(1, this, "Messenger with key {0} already added", messenger.Key); + this.LogWarning("Messenger with key {messengerKey) already added", messenger.Key); return; } @@ -1287,10 +1175,8 @@ namespace PepperDash.Essentials _roomBridges.Add(roomBridge); } - Debug.Console( - 2, - this, - "Adding messenger with key {0} for path {1}", + this.LogVerbose( + "Adding messenger with key {messengerKey} for path {messengerPath}", messenger.Key, messenger.MessagePath ); @@ -1304,10 +1190,8 @@ namespace PepperDash.Essentials { if (_defaultMessengers.ContainsKey(messenger.Key)) { - Debug.Console( - 1, - this, - "Default messenger with key {0} already added", + this.LogWarning( + "Default messenger with key {messengerKey} already added", messenger.Key ); return; @@ -1317,10 +1201,8 @@ namespace PepperDash.Essentials { simplMessenger.ConfigurationIsReady += Bridge_ConfigurationIsReady; } - Debug.Console( - 2, - this, - "Adding default messenger with key {0} for path {1}", + this.LogVerbose( + "Adding default messenger with key {messengerKey} for path {messengerPath}", messenger.Key, messenger.MessagePath ); @@ -1335,10 +1217,8 @@ namespace PepperDash.Essentials private void RegisterMessengerWithServer(IMobileControlMessenger messenger) { - Debug.Console( - 2, - this, - "Registering messenger with key {0} for path {1}", + this.LogVerbose( + "Registering messenger with key {messengerKey} for path {messengerPath}", messenger.Key, messenger.MessagePath ); @@ -1356,16 +1236,7 @@ namespace PepperDash.Essentials } catch (Exception ex) { - Debug.Console( - 0, - this, - $"Exception registering paths for {messenger.Key}: {ex.Message}" - ); - Debug.Console( - 2, - this, - $"Exception registering paths for {messenger.Key}: {ex.StackTrace}" - ); + this.LogException(ex, "Exception registering custom messenger {messengerKey}", messenger.Key); continue; } } @@ -1378,16 +1249,7 @@ namespace PepperDash.Essentials } catch (Exception ex) { - Debug.Console( - 0, - this, - $"Exception registering paths for {messenger.Key}: {ex.Message}" - ); - Debug.Console( - 2, - this, - $"Exception registering paths for {messenger.Key}: {ex.StackTrace}" - ); + this.LogException(ex, "Exception registering default messenger {messengerKey}", messenger.Key); continue; } } @@ -1417,7 +1279,7 @@ namespace PepperDash.Essentials } catch (Exception e) { - Debug.Console(0, "Unable to find MobileControlSystemController in Devices: {0}", e); + Debug.LogMessage(e, "Unable to find MobileControlSystemController in Devices"); return null; } } @@ -1435,10 +1297,7 @@ namespace PepperDash.Essentials if (string.IsNullOrEmpty(SystemUuid)) { - Debug.Console( - 0, - this, - Debug.ErrorLogLevel.Error, + this.LogError( "System UUID not defined. Unable to connect to Mobile Control" ); return false; @@ -1452,10 +1311,8 @@ namespace PepperDash.Essentials Log = { Output = (data, s) => - Debug.Console( - 1, - Debug.ErrorLogLevel.Notice, - "Message from websocket: {0}", + this.LogDebug( + "Message from websocket: {message}", data ) } @@ -1477,10 +1334,7 @@ namespace PepperDash.Essentials { if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Appliance) { - Debug.Console( - 0, - this, - Debug.ErrorLogLevel.Notice, + this.LogWarning( "System Monitor does not exist for this platform. Skipping..." ); return; @@ -1497,22 +1351,13 @@ namespace PepperDash.Essentials AddDeviceMessenger(messenger); } - /* public void CreateMobileControlRoomBridge(IEssentialsRoom room, IMobileControl parent) - { - var bridge = new MobileControlEssentialsRoomBridge(room); - AddBridgePostActivationAction(bridge); - DeviceManager.AddDevice(bridge); - } */ - #endregion private void SetWebsocketDebugLevel(string cmdparameters) { if (CrestronEnvironment.ProgramCompatibility == eCrestronSeries.Series4) { - Debug.Console( - 0, - this, + this.LogInformation( "Setting websocket log level not currently allowed on 4 series." ); return; // Web socket log level not currently allowed in series4 @@ -1520,22 +1365,14 @@ namespace PepperDash.Essentials if (string.IsNullOrEmpty(cmdparameters)) { - Debug.Console(0, this, "Current Websocket debug level: {0}", _wsLogLevel); + this.LogInformation("Current Websocket debug level: {webSocketDebugLevel}", _wsLogLevel); return; } if (cmdparameters.ToLower().Contains("help") || cmdparameters.ToLower().Contains("?")) { - Debug.Console( - 0, - this, - "valid options are:\r\n{0}\r\n{1}\r\n{2}\r\n{3}\r\n{4}\r\n{5}\r\n", - LogLevel.Trace, - LogLevel.Debug, - LogLevel.Info, - LogLevel.Warn, - LogLevel.Error, - LogLevel.Fatal + CrestronConsole.ConsoleCommandResponse( + $"valid options are:\r\n{LogLevel.Trace}\r\n{LogLevel.Debug}\r\n{LogLevel.Info}\r\n{LogLevel.Warn}\r\n{LogLevel.Error}\r\n{LogLevel.Fatal}\r\n" ); } @@ -1550,35 +1387,17 @@ namespace PepperDash.Essentials _wsClient2.Log.Level = _wsLogLevel; } - Debug.Console(0, this, "Websocket log level set to {0}", debugLevel); + CrestronConsole.ConsoleCommandResponse($"Websocket log level set to {debugLevel}"); } catch { - Debug.Console( - 0, - this, - "{0} is not a valid debug level. Valid options are: {1}, {2}, {3}, {4}, {5}, {6}", - cmdparameters, - LogLevel.Trace, - LogLevel.Debug, - LogLevel.Info, - LogLevel.Warn, - LogLevel.Error, - LogLevel.Fatal + CrestronConsole.ConsoleCommandResponse( + $"{cmdparameters} is not a valid debug level. Valid options are:\r\n{LogLevel.Trace}\r\n{LogLevel.Debug}\r\n{LogLevel.Info}\r\n{LogLevel.Warn}\r\n{LogLevel.Error}\r\n{LogLevel.Fatal}\r\n" ); + } } - /* private void AddBridgePostActivationAction(MobileControlBridgeBase bridge) - { - bridge.AddPostActivationAction(() => - { - Debug.Console(0, bridge, "Linking to parent controller"); - bridge.AddParent(this); - AddBridge(bridge); - }); - }*/ - /// /// Sends message to server to indicate the system is shutting down /// @@ -1653,11 +1472,7 @@ namespace PepperDash.Essentials ) ) { - Debug.Console( - 0, - this, - $"Messenger of type {messenger.GetType().Name} already exists. Skipping actions for {messenger.Key}" - ); + this.LogWarning("Messenger of type {messengerType} already exists. Skipping actions for {messengerKey}", messenger.GetType().Name, messenger.Key); return; } @@ -1702,7 +1517,7 @@ namespace PepperDash.Essentials /// private void Bridge_ConfigurationIsReady(object sender, EventArgs e) { - Debug.Console(1, this, "Bridge ready. Registering"); + this.LogDebug("Bridge ready. Registering"); // send the configuration object to the server @@ -1726,7 +1541,7 @@ namespace PepperDash.Essentials /// private void ReconnectToServerTimerCallback(object o) { - Debug.Console(1, this, "Attempting to reconnect to server..."); + this.LogDebug("Attempting to reconnect to server..."); ConnectWebsocketClient(); } @@ -1769,12 +1584,12 @@ namespace PepperDash.Essentials if (response.Authorized) { - Debug.Console(0, this, "System authorized, sending config."); + this.LogDebug("System authorized, sending config."); RegisterSystemToServer(); return; } - Debug.Console(0, this, response.Reason); + this.LogInformation(response.Reason); }); } @@ -1908,9 +1723,7 @@ Mobile Control Direct Server Infromation: if (!Config.EnableApiServer) { - Debug.Console( - 0, - this, + this.LogInformation( "ApiServer disabled via config. Cancelling attempt to register to server." ); return; @@ -1920,7 +1733,7 @@ Mobile Control Direct Server Infromation: if (!result) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "Unable to create websocket."); + this.LogFatal("Unable to create websocket."); return; } @@ -1957,11 +1770,8 @@ Mobile Control Direct Server Infromation: //Fires OnMessage event when PING is received. _wsClient2.EmitOnPing = true; - Debug.Console( - 1, - this, - Debug.ErrorLogLevel.Notice, - "Connecting mobile control client to {0}", + this.LogDebug( + "Connecting mobile control client to {mobileControlUrl}", _wsClient2.Url ); @@ -1985,26 +1795,21 @@ Mobile Control Direct Server Infromation: } catch (InvalidOperationException) { - Debug.Console( - 0, - Debug.ErrorLogLevel.Error, + this.LogError( "Maximum retries exceeded. Restarting websocket" ); HandleConnectFailure(); } catch (IOException ex) { - Debug.Console(0, this, Debug.ErrorLogLevel.Error, "IO Exception\r\n{0}", ex); + this.LogException(ex, "IO Exception on connect"); HandleConnectFailure(); } catch (Exception ex) { - Debug.Console( - 0, - Debug.ErrorLogLevel.Error, - "Error on Websocket Connect: {0}\r\nStack Trace: {1}", - ex.Message, - ex.StackTrace + this.LogException( + ex, + "Error on Websocket Connect" ); HandleConnectFailure(); } @@ -2024,10 +1829,8 @@ Mobile Control Direct Server Infromation: Log = { Output = (data, s) => - Debug.Console( - 1, - Debug.ErrorLogLevel.Notice, - "Message from websocket: {0}", + this.LogDebug( + "Message from websocket: {message}", data ) } @@ -2055,7 +1858,7 @@ Mobile Control Direct Server Infromation: { StopServerReconnectTimer(); StartPingTimer(); - Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Mobile Control API connected"); + this.LogInformation("Mobile Control API connected"); SendMessageObject(new MobileControlMessage { Type = "hello" }); } @@ -2087,7 +1890,8 @@ Mobile Control Direct Server Infromation: /// private void HandleError(object sender, ErrorEventArgs e) { - Debug.Console(1, this, "Websocket error {0}", e.Message); + this.LogError("Websocket error {0}", e.Message); + IsAuthorized = false; StartServerReconnectTimer(); } @@ -2099,11 +1903,8 @@ Mobile Control Direct Server Infromation: /// private void HandleClose(object sender, CloseEventArgs e) { - Debug.Console( - 1, - this, - Debug.ErrorLogLevel.Notice, - "Websocket close {0} {1}, clean={2}", + this.LogDebug( + "Websocket close {code} {reason}, clean={wasClean}", e.Code, e.Reason, e.WasClean @@ -2125,7 +1926,7 @@ Mobile Control Direct Server Infromation: /// private void SendInitialMessage() { - Debug.Console(1, this, "Sending initial join message"); + this.LogInformation("Sending initial join message"); var touchPanels = DeviceManager .AllDevices.OfType() @@ -2241,7 +2042,7 @@ Mobile Control Direct Server Infromation: return; } - Debug.Console(1, this, "Disconnecting websocket"); + this.LogDebug("Disconnecting websocket"); _wsClient2.Close(); } @@ -2272,10 +2073,8 @@ Mobile Control Direct Server Infromation: private void PingTimerCallback(object o) { - Debug.Console( - 1, - this, - Debug.ErrorLogLevel.Notice, + this.LogDebug( + "Ping timer expired. Closing websocket" ); @@ -2285,12 +2084,8 @@ Mobile Control Direct Server Infromation: } catch (Exception ex) { - Debug.Console( - 0, - Debug.ErrorLogLevel.Error, - "Exception closing websocket: {0}\r\nStack Trace: {1}", - ex.Message, - ex.StackTrace + this.LogException(ex, + "Exception closing websocket" ); HandleConnectFailure(); @@ -2307,7 +2102,7 @@ Mobile Control Direct Server Infromation: ReconnectToServerTimerCallback, ServerReconnectInterval ); - Debug.Console(1, this, "Reconnect Timer Started."); + this.LogDebug("Reconnect Timer Started."); } /// @@ -2343,21 +2138,6 @@ Mobile Control Direct Server Infromation: } } - //private void HandleClientJoined(JToken content) - //{ - // var clientId = content["clientId"].Value(); - // var roomKey = content["roomKey"].Value(); - - // SendMessageObject( - // new MobileControlMessage - // { - // Type = "/system/roomKey", - // ClientId = clientId, - // Content = roomKey - // } - // ); - //} - private void HandleClientJoined(JToken content) { var clientId = content["clientId"].Value(); @@ -2379,8 +2159,8 @@ Mobile Control Direct Server Infromation: if (!_roomCombiner.CurrentScenario.UiMap.ContainsKey(roomKey)) { - Debug.Console(0, this, - "Unable to find correct roomKey for {0} in current scenario. Returning {0} as roomKey", roomKey); + this.LogWarning( + "Unable to find correct roomKey for {roomKey} in current scenario. Returning {roomKey} as roomKey", roomKey); var message = new MobileControlMessage { @@ -2418,14 +2198,7 @@ Mobile Control Direct Server Infromation: catch { qrChecksum = new JValue(string.Empty); - } - - Debug.Console( - 1, - this, - "QR checksum: {0}", - qrChecksum == null ? string.Empty : qrChecksum.Value() - ); + } if (code == null) { @@ -2462,10 +2235,8 @@ Mobile Control Direct Server Infromation: if (!messageText.Contains("/system/heartbeat")) { - Debug.LogMessage( - LogEventLevel.Debug, + this.LogDebug( "Message RX: {messageText}", - this, messageText ); } @@ -2499,7 +2270,7 @@ Mobile Control Direct Server Infromation: DeviceJsonApi.DoDeviceAction(wrapper); break; case "close": - Debug.Console(1, this, "Received close message from server."); + this.LogDebug("Received close message from server"); break; default: // Incoming message example @@ -2535,10 +2306,9 @@ Mobile Control Direct Server Infromation: } catch (Exception err) { - Debug.LogMessage( + this.LogException( err, "Unable to parse {message}", - this, messageText ); } diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs index 8febf414..ce1864b6 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlSIMPLRoomBridge.cs @@ -5,6 +5,7 @@ using Crestron.SimplSharpPro.EthernetCommunication; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; @@ -592,7 +593,7 @@ namespace PepperDash.Essentials.Room.MobileControl if (!Eisc.BooleanOutput[JoinMap.UseSourceEnabled.JoinNumber].BoolValue && string.IsNullOrEmpty(name)) { - Debug.Console(1, "Source at join {0} does not have a name", JoinMap.SourceNameJoinStart.JoinNumber + i); + this.LogDebug("Source at join {join} does not have a name", JoinMap.SourceNameJoinStart.JoinNumber + i); break; } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs index 52fa693b..dd69d2da 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.DeviceTypeInterfaces; @@ -19,7 +20,7 @@ namespace PepperDash.Essentials.Touchpanel { if (_appControl == null) { - Debug.Console(0, this, $"{_device.Key} does not implement ITswAppControl"); + this.LogInformation("{deviceKey} does not implement ITswAppControl", _device.Key); return; } diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs index da59d93c..66ed4c1f 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.DeviceTypeInterfaces; @@ -20,7 +21,7 @@ namespace PepperDash.Essentials.Touchpanel { if (_zoomControl == null) { - Debug.Console(0, this, $"{_device.Key} does not implement ITswZoomControl"); + this.LogInformation("{deviceKey} does not implement ITswZoomControl", _device.Key); return; } diff --git a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs index 76c5f1b3..6e536f4a 100644 --- a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs +++ b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs @@ -1,8 +1,10 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.Queues; +using Serilog.Events; using System; using System.Threading; using WebSocketSharp; @@ -32,40 +34,32 @@ namespace PepperDash.Essentials { try { - - //Debug.Console(2, "Dispatching message type: {0}", msgToSend.GetType()); - - //Debug.Console(2, "Message: {0}", msgToSend.ToString()); - - //var messageToSend = JObject.FromObject(msgToSend); - - if (_ws != null && _ws.IsAlive) + if (_ws == null) { - var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); - - Debug.Console(2, "Message TX: {0}", message); - - _ws.Send(message); + Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is null"); + return; } - else if (_ws == null) + + if (!_ws.IsAlive) { - Debug.Console(1, "Cannot send. No client."); + Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Websocket client is not connected"); + return; } + + + var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); + + Debug.LogMessage(LogEventLevel.Verbose, "Message TX: {0}", null, message); + + _ws.Send(message); + + } catch (Exception ex) { - Debug.Console(0, Debug.ErrorLogLevel.Error, "Caught an exception in the Transmit Processor {0}\r{1}\r{2}", ex.Message, ex.InnerException, ex.StackTrace); - Debug.Console(2, Debug.ErrorLogLevel.Error, "Stack Trace: {0}", ex.StackTrace); - - if (ex.InnerException != null) - { - Debug.Console(0, Debug.ErrorLogLevel.Error, "Inner Exception: {0}", ex.InnerException.Message); - Debug.Console(2, Debug.ErrorLogLevel.Error, "Stack Trace: {0}", ex.InnerException.StackTrace); - } + Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); } - - } #endregion } @@ -95,35 +89,33 @@ namespace PepperDash.Essentials { try { - //Debug.Console(2, "Message: {0}", msgToSend.ToString()); - - if (_server != null) + if(_server == null) { - Debug.Console(2, _server, Debug.ErrorLogLevel.Notice, "Dispatching message type: {0}", msgToSend.GetType()); - - var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, - new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); - - var clientSpecificMessage = msgToSend as MobileControlMessage; - if (clientSpecificMessage.ClientId != null) - { - var clientId = clientSpecificMessage.ClientId; - - Debug.Console(2, _server, "Message TX To Client ID: {0} Message: {1}", clientId, message); - - _server.SendMessageToClient(clientId, message); - } - else - { - _server.SendMessageToAllClients(message); - - Debug.Console(2, "Message TX To Clients: {0}", message); - } + Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null"); + return; } - else if (_server == null) + + var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); + + var clientSpecificMessage = msgToSend as MobileControlMessage; + if (clientSpecificMessage.ClientId != null) { - Debug.Console(1, "Cannot send. No server."); + var clientId = clientSpecificMessage.ClientId; + + _server.LogVerbose("Message TX To client {clientId} Message: {message}", clientId, message); + + _server.SendMessageToClient(clientId, message); + + return; } + + _server.SendMessageToAllClients(message); + + _server.LogVerbose("Message TX To all clients: {message}", null, message); + + + } catch (ThreadAbortException) { @@ -131,14 +123,7 @@ namespace PepperDash.Essentials } catch (Exception ex) { - Debug.Console(0, Debug.ErrorLogLevel.Error, "Caught an exception in the Transmit Processor {0}", ex.Message); - Debug.Console(2, Debug.ErrorLogLevel.Error, "Stack Trace: {0}", ex.StackTrace); - - if (ex.InnerException != null) - { - Debug.Console(0, Debug.ErrorLogLevel.Error, "----\r\n{0}", ex.InnerException.Message); - Debug.Console(2, Debug.ErrorLogLevel.Error, "Stack Trace: {0}", ex.InnerException.StackTrace); - } + Debug.LogMessage(ex,"Caught an exception in the Transmit Processor"); } diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs index 537dafb9..56d61347 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Web; +using Serilog.Events; namespace PepperDash.Essentials.WebApiHandlers { @@ -113,7 +114,7 @@ namespace PepperDash.Essentials.WebApiHandlers if (!server.Server.RemoveWebSocketService(path)) { - Debug.Console(0, $"Unable to remove client with token {request.Token}"); + Debug.LogMessage(LogEventLevel.Warning, "Unable to remove client with token {token}", request.Token); var response = new ClientResponse { diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index b1aa483e..ee9ff1f3 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -3,11 +3,13 @@ using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.Web; using PepperDash.Essentials.WebApiHandlers; +using Serilog.Events; using System; using System.Collections.Generic; using System.IO; @@ -61,7 +63,7 @@ namespace PepperDash.Essentials base.OnOpen(); var url = Context.WebSocket.Url; - Debug.Console(2, Debug.ErrorLogLevel.Notice, "New WebSocket Connection from: {0}", url); + Debug.LogMessage(LogEventLevel.Verbose, "New WebSocket Connection from: {0}", null, url); var match = Regex.Match(url.AbsoluteUri, "(?:ws|wss):\\/\\/.*(?:\\/mc\\/api\\/ui\\/join\\/)(.*)"); @@ -76,7 +78,7 @@ namespace PepperDash.Essentials if (Controller == null) { - Debug.Console(2, "WebSocket UiClient Controller is null"); + Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Controller is null"); _connectionTime = DateTime.Now; } @@ -91,37 +93,6 @@ namespace PepperDash.Essentials }; Controller.HandleClientMessage(JsonConvert.SerializeObject(clientJoinedMessage)); - // Inform controller of client joining - /* - var clientJoined = new MobileControlMessage - { - Type = "/system/roomKey", - ClientId = clientId, - Content = RoomKey, - }; - - Controller.SendMessageObjectToDirectClient(clientJoined); - - if (Controller.Config.EnableUiMirroring) - { - var uiMirrorEnabled = new MobileControlMessage - { - Type = "/system/uiMirrorEnabled", - ClientId = clientId, - Content = JToken.FromObject(new MobileControlSimpleContent { Value = Controller.Config.EnableUiMirroring }), - }; - - var uiState = new MobileControlMessage - { - Type = "/system/uiMirrorState", - ClientId = clientId, - Content = Controller.LastUiState, - }; - - Controller.SendMessageObjectToDirectClient(uiMirrorEnabled); - - Controller.SendMessageObjectToDirectClient(uiState); - }*/ var bridge = Controller.GetRoomBridge(RoomKey); @@ -173,15 +144,14 @@ namespace PepperDash.Essentials { base.OnClose(e); - Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Closing: {0} reason: {1}", e.Code, e.Reason); - + Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Closing: {0} reason: {1}", null, e.Code, e.Reason); } protected override void OnError(ErrorEventArgs e) { base.OnError(e); - Debug.Console(2, Debug.ErrorLogLevel.Notice, "WebSocket UiClient Error: {0} message: {1}", e.Exception, e.Message); + Debug.LogMessage(LogEventLevel.Verbose, "WebSocket UiClient Error: {exception} message: {message}", e.Exception, e.Message); } } @@ -333,7 +303,7 @@ namespace PepperDash.Essentials if (apiServer == null) { - Debug.Console(0, this, "No API Server available"); + this.LogInformation("No API Server available"); return; } @@ -423,7 +393,7 @@ namespace PepperDash.Essentials if (bridge == null) { - Debug.Console(0, this, $"Unable to find room with key: {client.DefaultRoomKey}"); + this.LogWarning("Unable to find room with key: {defaultRoomKey}", client.DefaultRoomKey); return; } @@ -431,7 +401,7 @@ namespace PepperDash.Essentials if (key == null) { - Debug.Console(0, this, $"Unable to generate a client for {client.Key}"); + this.LogWarning("Unable to generate a client for {clientKey}", client.Key); continue; } } @@ -440,7 +410,7 @@ namespace PepperDash.Essentials var processorIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, lanAdapterId); - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, $"Processor IP: {processorIp}", this); + this.LogVerbose("Processor IP: {processorIp}", processorIp); foreach (var touchpanel in touchpanels.Select(tp => { @@ -453,19 +423,19 @@ namespace PepperDash.Essentials { if (touchpanel.Key == null) { - Debug.Console(0, this, $"Token for touchpanel {touchpanel.Touchpanel.Key} not found"); + this.LogWarning("Token for touchpanel {touchpanelKey} not found", touchpanel.Touchpanel.Key); continue; } if (touchpanel.Messenger == null) { - Debug.Console(2, this, $"Unable to find room messenger for {touchpanel.Touchpanel.DefaultRoomKey}"); + this.LogWarning("Unable to find room messenger for {defaultRoomKey}", touchpanel.Touchpanel.DefaultRoomKey); continue; } var appUrl = $"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"; - Debug.Console(2, this, $"Sending URL {appUrl}"); + this.LogVerbose("Sending URL {appUrl}", appUrl); touchpanel.Messenger.UpdateAppUrl($"http://{processorIp}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}"); } @@ -737,7 +707,7 @@ namespace PepperDash.Essentials if (bridge == null) { - Debug.Console(0, this, $"Unable to find room with key: {roomKey}"); + this.LogWarning("Unable to find room with key: {roomKey}", roomKey); return (null, null); } @@ -926,7 +896,7 @@ namespace PepperDash.Essentials var path = req.RawUrl; - Debug.Console(2, this, "GET Request received at path: {0}", path); + this.LogVerbose("GET Request received at path: {path}", path); // Call for user app to join the room with a token if (path.StartsWith("/mc/api/ui/joinroom")) @@ -972,9 +942,9 @@ namespace PepperDash.Essentials var path = req.RawUrl; var ip = req.RemoteEndPoint.Address.ToString(); - Debug.Console(2, this, "POST Request received at path: {0} from host {1}", path, ip); + this.LogVerbose("POST Request received at path: {path} from host {host}", path, ip); - var body = new System.IO.StreamReader(req.InputStream).ReadToEnd(); + var body = new StreamReader(req.InputStream).ReadToEnd(); if (path.StartsWith("/mc/api/log")) { @@ -990,7 +960,7 @@ namespace PepperDash.Essentials await LogClient.SendAsync(logRequest); - Debug.Console(2, this, "Log data sent to {0}:{1}", _parent.Config.DirectServer.Logging.Host, _parent.Config.DirectServer.Logging.Port); + this.LogVerbose("Log data sent to {host}:{port}", _parent.Config.DirectServer.Logging.Host, _parent.Config.DirectServer.Logging.Port); } else { @@ -1000,7 +970,7 @@ namespace PepperDash.Essentials } catch (Exception ex) { - Debug.LogMessage(ex, "Caught an exception in the OnPost handler", this); + this.LogException(ex, "Caught an exception in the OnPost handler"); } } @@ -1033,7 +1003,7 @@ namespace PepperDash.Essentials var qp = req.QueryString; var token = qp["token"]; - Debug.Console(2, this, "Join Room Request with token: {0}", token); + this.LogVerbose("Join Room Request with token: {token}", token); if (UiClients.TryGetValue(token, out UiClientContext clientContext)) @@ -1074,10 +1044,11 @@ namespace PepperDash.Essentials var message = string.Format("Unable to find bridge with key: {0}", clientContext.Token.RoomKey); res.StatusCode = 404; res.ContentType = "application/json"; + this.LogVerbose("{message}", message); var body = Encoding.UTF8.GetBytes(message); res.ContentLength64 = body.LongLength; res.Close(body, true); - Debug.Console(2, this, "{0}", message); + } } else @@ -1085,7 +1056,7 @@ namespace PepperDash.Essentials var message = "Token invalid or has expired"; res.StatusCode = 401; res.ContentType = "application/json"; - Debug.Console(2, this, "{0}", message); + this.LogVerbose("{message}", message); var body = Encoding.UTF8.GetBytes(message); res.ContentLength64 = body.LongLength; res.Close(body, true); @@ -1102,7 +1073,7 @@ namespace PepperDash.Essentials res.ContentType = "application/json"; var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion }; var message = JsonConvert.SerializeObject(version); - Debug.Console(2, this, "{0}", message); + this.LogVerbose("{message}", message); var body = Encoding.UTF8.GetBytes(message); res.ContentLength64 = body.LongLength; @@ -1118,7 +1089,7 @@ namespace PepperDash.Essentials { var path = req.RawUrl; - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Requesting Image: {0}", this, path); + Debug.LogMessage(LogEventLevel.Verbose, "Requesting Image: {0}", this, path); var imageBasePath = Global.DirectorySeparator + "html" + Global.DirectorySeparator + "logo" + Global.DirectorySeparator; @@ -1126,9 +1097,9 @@ namespace PepperDash.Essentials var filePath = imageBasePath + image; - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Retrieving Image: {0}", this, filePath); + Debug.LogMessage(LogEventLevel.Verbose, "Retrieving Image: {0}", this, filePath); - if (System.IO.File.Exists(filePath)) + if (File.Exists(filePath)) { if(filePath.EndsWith(".png")) { @@ -1165,7 +1136,7 @@ namespace PepperDash.Essentials /// private void HandleUserAppRequest(HttpListenerRequest req, HttpListenerResponse res, string path) { - Debug.Console(2, this, "Requesting User app file..."); + this.LogVerbose("Requesting User app file"); var qp = req.QueryString; var token = qp["token"]; @@ -1189,7 +1160,7 @@ namespace PepperDash.Essentials // swap the base href prefix for the file path prefix filePath = filePath.Replace(_userAppBaseHref, _appPath); - Debug.Console(2, this, "filepath: {0}", filePath); + this.LogVerbose("filepath: {filePath}", filePath); // append index.html if no specific file is specified @@ -1208,7 +1179,7 @@ namespace PepperDash.Essentials // Set ContentType based on file type if (filePath.EndsWith(".html")) { - Debug.Console(2, this, "Client requesting User App..."); + this.LogVerbose("Client requesting User App"); res.ContentType = "text/html"; } @@ -1228,17 +1199,17 @@ namespace PepperDash.Essentials } } - Debug.Console(2, this, "Attempting to serve file: {0}", filePath); + this.LogVerbose("Attempting to serve file: {filePath}", filePath); byte[] contents; if (System.IO.File.Exists(filePath)) { - Debug.Console(2, this, "File found"); + this.LogVerbose("File found: {filePath}", filePath); contents = System.IO.File.ReadAllBytes(filePath); } else { - Debug.Console(2, this, "File not found: {0}", filePath); + this.LogVerbose("File not found: {filePath}", filePath); res.StatusCode = (int)HttpStatusCode.NotFound; res.Close(); return; @@ -1250,7 +1221,7 @@ namespace PepperDash.Essentials public void StopServer() { - Debug.Console(2, this, "Stopping WebSocket Server"); + this.LogVerbose("Stopping WebSocket Server"); _server.Stop(CloseStatusCode.Normal, "Server Shutting Down"); } @@ -1295,7 +1266,7 @@ namespace PepperDash.Essentials } else { - Debug.Console(0, this, "Unable to find client with ID: {0}", clientId); + this.LogWarning("Unable to find client with ID: {clientId}", clientId); } } } From 789111cb9a7f386d4eac15e48d1325f95569f1bd Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 22:14:15 -0500 Subject: [PATCH 16/26] style: run code cleanup & apply VS suggestions --- .../ContentTypes.cs | 2 +- .../DisplayBaseMessenger.cs | 6 +- .../DeviceTypeExtensions/IChannelMessenger.cs | 6 +- .../DeviceTypeExtensions/IColorMessenger.cs | 7 +- .../DeviceTypeExtensions/IDPadMessenger.cs | 4 +- .../DeviceTypeExtensions/IDvrMessenger.cs | 4 +- .../IHasPowerMessenger.cs | 3 +- .../DeviceTypeExtensions/INumericMessenger.cs | 4 +- .../ISetTopBoxControlsMessenger.cs | 8 +- .../ITransportMessenger.cs | 18 +-- .../Messengers/AudioCodecBaseMessenger.cs | 15 +- .../Messengers/CameraBaseMessenger.cs | 23 ++- .../Messengers/DeviceInfoMessenger.cs | 5 - .../Messengers/DevicePresetsModelMessenger.cs | 6 +- .../Messengers/DeviceVolumeMessenger.cs | 17 +-- .../Messengers/GenericMessenger.cs | 2 - .../ICommunicationMonitorMessenger.cs | 9 +- .../IEssentialsRoomCombinerMessenger.cs | 4 +- .../IHasCurrentSourceInfoMessenger.cs | 8 +- .../IHasPowerControlWithFeedbackMessenger.cs | 17 +-- .../IHasScheduleAwarenessMessenger.cs | 13 +- .../Messengers/ILevelControlsMessenger.cs | 10 +- .../Messengers/IMatrixRoutingMessenger.cs | 16 +-- .../IProjectorScreenLiftControlMessenger.cs | 2 +- .../Messengers/IRunRouteActionMessenger.cs | 1 - .../Messengers/ISelectableItemsMessenger.cs | 9 +- .../Messengers/ISwitchedOutputMessenger.cs | 13 +- .../Messengers/ITechPasswordMessenger.cs | 13 +- .../Messengers/LightingBaseMessenger.cs | 1 - .../Messengers/MessengerBase.cs | 21 +-- .../Messengers/PressAndHoldHandler.cs | 10 +- .../Messengers/RoomEventScheduleMessenger.cs | 1 - .../Messengers/SIMPLAtcMessenger.cs | 19 ++- .../Messengers/SIMPLCameraMessenger.cs | 10 +- .../Messengers/SIMPLDirectRouteMessenger.cs | 1 - .../Messengers/SIMPLRouteMessenger.cs | 6 +- .../Messengers/SIMPLVtcMessenger.cs | 3 +- .../Messengers/ShadeBaseMessenger.cs | 1 - .../Messengers/SystemMonitorMessenger.cs | 17 ++- .../Messengers/TwoWayDisplayBaseMessenger.cs | 24 ++-- .../Interfaces.cs | 4 +- .../MobileControlAction.cs | 10 +- .../MobileControlConfig.cs | 3 +- .../MobileControlDeviceFactory.cs | 7 +- .../MobileControlFactory.cs | 10 +- .../MobileControlSystemController.cs | 134 +++++++++--------- ...PepperDash.Essentials.MobileControl.csproj | 11 +- .../RoomBridges/MobileControlBridgeBase.cs | 7 +- .../MobileControlEssentialsRoomBridge.cs | 20 +-- .../Services/MobileControlApiService.cs | 3 +- .../Touchpanel/ITheme.cs | 10 +- .../Touchpanel/ITswAppControlMessenger.cs | 1 - .../Touchpanel/ITswZoomControlMessenger.cs | 3 +- .../MobileControlTouchpanelController.cs | 12 +- .../MobileControlTouchpanelProperties.cs | 2 +- .../Touchpanel/ThemeMessenger.cs | 6 +- .../TransmitMessage.cs | 23 +-- .../UserCodeChangedContent.cs | 2 +- .../Volumes.cs | 2 +- .../WebApiHandlers/ActionPathsHandler.cs | 3 +- .../WebApiHandlers/MobileInfoHandler.cs | 6 +- .../WebApiHandlers/UiClientHandler.cs | 1 + .../MobileControlWebsocketServer.cs | 78 +++++----- .../WebSocketServerSecretProvider.cs | 2 +- 64 files changed, 329 insertions(+), 390 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs b/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs index 476747b0..e555f11f 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/ContentTypes.cs @@ -5,7 +5,7 @@ namespace PepperDash.Essentials.AppServer { public class SourceSelectMessageContent { - + [JsonProperty("sourceListItemKey")] public string SourceListItemKey { get; set; } [JsonProperty("sourceListKey")] diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs index 659c62e8..3d15db76 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs @@ -2,14 +2,12 @@ using PepperDash.Core; using PepperDash.Essentials.AppServer; using PepperDash.Essentials.AppServer.Messengers; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System; using System.Linq; using DisplayBase = PepperDash.Essentials.Devices.Common.Displays.DisplayBase; namespace PepperDash.Essentials.Room.MobileControl { - public class DisplayBaseMessenger: MessengerBase + public class DisplayBaseMessenger : MessengerBase { private readonly DisplayBase display; @@ -20,7 +18,7 @@ namespace PepperDash.Essentials.Room.MobileControl protected override void RegisterActions() { - base.RegisterActions(); + base.RegisterActions(); /*AddAction("/powerOn", (id, content) => display.PowerOn()); AddAction("/powerOff", (id, content) => display.PowerOff()); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs index d02ecbf3..3cc382f8 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs @@ -4,7 +4,7 @@ using PepperDash.Essentials.Core; namespace PepperDash.Essentials.Room.MobileControl { - public class IChannelMessenger:MessengerBase + public class IChannelMessenger : MessengerBase { private readonly IChannel channelDevice; @@ -15,14 +15,14 @@ namespace PepperDash.Essentials.Room.MobileControl protected override void RegisterActions() { - base.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("/info", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => channelDevice?.Info(b))); AddAction("/exit", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => channelDevice?.Exit(b))); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs index d23fbf2b..021f8a25 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs @@ -1,11 +1,10 @@ using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; namespace PepperDash.Essentials.Room.MobileControl { - public class IColorMessenger:MessengerBase + public class IColorMessenger : MessengerBase { private readonly IColor colorDevice; public IColorMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) @@ -15,12 +14,12 @@ namespace PepperDash.Essentials.Room.MobileControl protected override void RegisterActions() { - base.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))); + 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 index 000f9185..1233d0da 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs @@ -4,7 +4,7 @@ using PepperDash.Essentials.Core; namespace PepperDash.Essentials.Room.MobileControl { - public class IDPadMessenger:MessengerBase + public class IDPadMessenger : MessengerBase { private readonly IDPad dpadDevice; public IDPadMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) @@ -15,7 +15,7 @@ namespace PepperDash.Essentials.Room.MobileControl protected override void RegisterActions() { - base.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))); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs index b722bd73..2fd4679c 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs @@ -4,7 +4,7 @@ using PepperDash.Essentials.Core; namespace PepperDash.Essentials.Room.MobileControl { - public class IDvrMessenger: MessengerBase + public class IDvrMessenger : MessengerBase { private readonly IDvr dvrDevice; public IDvrMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) @@ -14,7 +14,7 @@ namespace PepperDash.Essentials.Room.MobileControl protected override void RegisterActions() { - base.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))); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs index 33ce7ea1..8fe09e75 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs @@ -1,11 +1,10 @@ using PepperDash.Core; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; namespace PepperDash.Essentials.Room.MobileControl { - public class IHasPowerMessenger:MessengerBase + public class IHasPowerMessenger : MessengerBase { private readonly IHasPowerControl powerDevice; public IHasPowerMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs index 6f601013..490b02b8 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs @@ -4,7 +4,7 @@ using PepperDash.Essentials.Core; namespace PepperDash.Essentials.Room.MobileControl { - public class INumericKeypadMessenger:MessengerBase + public class INumericKeypadMessenger : MessengerBase { private readonly INumericKeypad keypadDevice; public INumericKeypadMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) @@ -14,7 +14,7 @@ namespace PepperDash.Essentials.Room.MobileControl protected override void RegisterActions() { - base.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))); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs index c47b5f47..16899110 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs @@ -4,7 +4,7 @@ using PepperDash.Essentials.Core; namespace PepperDash.Essentials.Room.MobileControl { - public class ISetTopBoxControlsMessenger:MessengerBase + public class ISetTopBoxControlsMessenger : MessengerBase { private readonly ISetTopBoxControls stbDevice; public ISetTopBoxControlsMessenger(string key, string messagePath, IKeyName device) : base(key, messagePath, device) @@ -18,15 +18,15 @@ namespace PepperDash.Essentials.Room.MobileControl 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()); + PostStatusMessage(new SetTopBoxControlsState()); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs index cf34b490..ee47491b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs @@ -4,7 +4,7 @@ using PepperDash.Essentials.Core; namespace PepperDash.Essentials.Room.MobileControl { - public class ITransportMessenger:MessengerBase + public class ITransportMessenger : MessengerBase { private readonly ITransport transportDevice; public ITransportMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) @@ -16,14 +16,14 @@ namespace PepperDash.Essentials.Room.MobileControl { 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))); + 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))); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs index 2acb90b5..9a42141e 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/AudioCodecBaseMessenger.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json.Linq; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Devices.Common.AudioCodec; using PepperDash.Essentials.Devices.Common.Codec; using System; @@ -103,14 +102,14 @@ namespace PepperDash.Essentials.AppServer.Messengers var info = Codec.CodecInfo; PostStatusMessage(JToken.FromObject(new + { + isInCall = Codec.IsInCall, + calls = Codec.ActiveCalls, + info = new { - isInCall = Codec.IsInCall, - calls = Codec.ActiveCalls, - info = new - { - phoneNumber = info.PhoneNumber - } - }) + phoneNumber = info.PhoneNumber + } + }) ); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs index 404dbf6c..36a94781 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/CameraBaseMessenger.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json.Linq; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Devices.Common.Cameras; using System; using System.Collections.Generic; @@ -40,9 +39,9 @@ namespace PepperDash.Essentials.AppServer.Messengers presetList = presetsCamera.Presets; PostStatusMessage(JToken.FromObject(new - { - presets = presetList - }) + { + presets = presetList + }) ); } @@ -175,14 +174,14 @@ namespace PepperDash.Essentials.AppServer.Messengers 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 - }) + { + cameraManualSupported = Camera is IHasCameraControls, + cameraAutoSupported = Camera is IHasCameraAutoMode, + cameraOffSupported = Camera is IHasCameraOff, + cameraMode = GetCameraMode(), + hasPresets = Camera is IHasCameraPresets, + presets = presetList + }) ); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs index 8a17ce01..ffd58948 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceInfoMessenger.cs @@ -2,11 +2,6 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core.DeviceInfo; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Essentials.AppServer.Messengers { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs index ee4aac53..148b7d5b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DevicePresetsModelMessenger.cs @@ -43,12 +43,14 @@ namespace PepperDash.Essentials.AppServer.Messengers protected override void RegisterActions() { - AddAction("/presets/fullStatus", (id, content) => { + AddAction("/presets/fullStatus", (id, content) => + { this.LogInformation("getting full status for client {id}", id); try { SendPresets(); - } catch(Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Exception sending preset full status", this); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs index 6531424e..22f837c3 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/DeviceVolumeMessenger.cs @@ -2,7 +2,6 @@ using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using PepperDash.Core; -using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using System; @@ -39,7 +38,8 @@ namespace PepperDash.Essentials.AppServer.Messengers } PostStatusMessage(messageObj); - } catch(Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Exception sending full status", this); } @@ -60,7 +60,7 @@ namespace PepperDash.Essentials.AppServer.Messengers }); AddAction("/muteToggle", (id, content) => - { + { _localDevice.MuteToggle(); }); @@ -74,21 +74,22 @@ namespace PepperDash.Essentials.AppServer.Messengers _localDevice.MuteOff(); }); - AddAction("/volumeUp", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => + 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) + } + catch (Exception ex) { Debug.LogMessage(ex, "Got exception during volume up: {Exception}", null, ex); } - })); + })); - AddAction("/volumeDown", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => + AddAction("/volumeDown", (id, content) => PressAndHoldHandler.HandlePressAndHold(DeviceKey, content, (b) => { Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Calling {localDevice} volume down with {value}", DeviceKey, b); @@ -132,7 +133,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } }; - PostStatusMessage(JToken.FromObject(message)); + PostStatusMessage(JToken.FromObject(message)); }; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs index 640df31a..64624bfa 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/GenericMessenger.cs @@ -1,6 +1,4 @@ using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System; namespace PepperDash.Essentials.AppServer.Messengers { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs index 7e4d03f6..5ab81832 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ICommunicationMonitorMessenger.cs @@ -3,11 +3,6 @@ using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Essentials.AppServer.Messengers { @@ -24,7 +19,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { base.RegisterActions(); - AddAction("/fullStatus", (id, content) => + AddAction("/fullStatus", (id, content) => { PostStatusMessage(new CommunicationMonitorState { @@ -33,7 +28,7 @@ namespace PepperDash.Essentials.AppServer.Messengers IsOnline = _communicationMonitor.CommunicationMonitor.IsOnline, Status = _communicationMonitor.CommunicationMonitor.Status } - }); + }); }); _communicationMonitor.CommunicationMonitor.StatusChange += (sender, args) => diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs index 9601bad6..2cedf5fa 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs @@ -79,7 +79,7 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(JToken.FromObject(message)); }; - foreach(var partition in _roomCombiner.Partitions) + foreach (var partition in _roomCombiner.Partitions) { partition.PartitionPresentFeedback.OutputChange += (sender, args) => { @@ -101,7 +101,7 @@ namespace PepperDash.Essentials.AppServer.Messengers foreach (var room in _roomCombiner.Rooms) { - rooms.Add(new RoomCombinerRoom{ Key = room.Key, Name = room.Name }); + rooms.Add(new RoomCombinerRoom { Key = room.Key, Name = room.Name }); } var message = new IEssentialsRoomCombinerStateMessage diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs index 65bc67bd..24f1f461 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasCurrentSourceInfoMessenger.cs @@ -2,7 +2,6 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.Routing; namespace PepperDash.Essentials.AppServer.Messengers { @@ -29,7 +28,8 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(message); }); - sourceDevice.CurrentSourceChange += (sender, e) => { + sourceDevice.CurrentSourceChange += (sender, e) => + { switch (e) { case ChangeType.DidChange: @@ -42,11 +42,11 @@ namespace PepperDash.Essentials.AppServer.Messengers break; } } - }; + }; } } - public class CurrentSourceStateMessage: DeviceStateMessageBase + public class CurrentSourceStateMessage : DeviceStateMessageBase { [JsonProperty("currentSourceKey", NullValueHandling = NullValueHandling.Ignore)] public string CurrentSourceKey { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs index bf838e92..689b5102 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs @@ -1,20 +1,15 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using PepperDash.Essentials.Core; using PepperDash.Core; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using PepperDash.Essentials.Core; namespace PepperDash.Essentials.AppServer.Messengers { - public class IHasPowerControlWithFeedbackMessenger: MessengerBase + public class IHasPowerControlWithFeedbackMessenger : MessengerBase { private readonly IHasPowerControlWithFeedback _powerControl; - public IHasPowerControlWithFeedbackMessenger(string key, string messagePath, IHasPowerControlWithFeedback powerControl) + public IHasPowerControlWithFeedbackMessenger(string key, string messagePath, IHasPowerControlWithFeedback powerControl) : base(key, messagePath, powerControl as Device) { _powerControl = powerControl; @@ -42,9 +37,9 @@ namespace PepperDash.Essentials.AppServer.Messengers private void PowerIsOnFeedback_OutputChange(object sender, FeedbackEventArgs args) { PostStatusMessage(JToken.FromObject(new - { - powerState = args.BoolValue - }) + { + powerState = args.BoolValue + }) ); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs index 550f1045..86529e1b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Devices.Common.Codec; using System; using System.Collections.Generic; @@ -28,13 +27,13 @@ namespace PepperDash.Essentials.AppServer.Messengers private void CodecSchedule_MeetingEventChange(object sender, MeetingEventArgs e) { PostStatusMessage(JToken.FromObject(new MeetingChangeMessage + { + MeetingChange = new MeetingChange { - MeetingChange = new MeetingChange - { - ChangeType = e.ChangeType.ToString(), - Meeting = e.Meeting - } - }) + ChangeType = e.ChangeType.ToString(), + Meeting = e.Meeting + } + }) ); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs index 6410d314..a476e9bc 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs @@ -1,13 +1,9 @@ -using Independentsoft.Exchange; -using Newtonsoft.Json; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Essentials.AppServer.Messengers { @@ -33,7 +29,7 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(message); }); - foreach(var levelControl in levelControlsDevice.LevelControlPoints) + foreach (var levelControl in levelControlsDevice.LevelControlPoints) { // reassigning here just in case of lambda closure issues var key = levelControl.Key; @@ -78,7 +74,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } } - public class LevelControlStateMessage:DeviceStateMessageBase + public class LevelControlStateMessage : DeviceStateMessageBase { [JsonProperty("levelControls")] public Dictionary Levels { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs index 8bacab8f..4e403261 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs @@ -3,10 +3,10 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Routing; -using System.Collections.Generic; -using System.Linq; using Serilog.Events; using System; +using System.Collections.Generic; +using System.Linq; namespace PepperDash.Essentials.AppServer.Messengers { @@ -36,7 +36,7 @@ namespace PepperDash.Essentials.AppServer.Messengers Inputs = matrixDevice.InputSlots.ToDictionary(kvp => kvp.Key, kvp => new RoutingInput(kvp.Value)), }; - + PostStatusMessage(message); } catch (Exception e) @@ -52,7 +52,7 @@ namespace PepperDash.Essentials.AppServer.Messengers matrixDevice.Route(request.InputKey, request.OutputKey, request.RouteType); }); - foreach(var output in matrixDevice.OutputSlots) + foreach (var output in matrixDevice.OutputSlots) { var key = output.Key; var outputSlot = output.Value; @@ -66,7 +66,7 @@ namespace PepperDash.Essentials.AppServer.Messengers }; } - foreach(var input in matrixDevice.InputSlots) + foreach (var input in matrixDevice.InputSlots) { var key = input.Key; var inputSlot = input.Value; @@ -82,7 +82,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } } - public class MatrixStateMessage : DeviceStateMessageBase + public class MatrixStateMessage : DeviceStateMessageBase { [JsonProperty("outputs")] public Dictionary Outputs; @@ -113,13 +113,13 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("videoSyncDetected", NullValueHandling = NullValueHandling.Ignore)] - public bool? VideoSyncDetected => _input?.VideoSyncDetected; + public bool? VideoSyncDetected => _input?.VideoSyncDetected; [JsonProperty("key", NullValueHandling = NullValueHandling.Ignore)] public string Key => _input?.Key; public RoutingInput(IRoutingInputSlot input) - { + { _input = input; } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs index 9d4a3804..af885639 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs @@ -7,7 +7,7 @@ using System; namespace PepperDash.Essentials.AppServer.Messengers { - public class IProjectorScreenLiftControlMessenger: MessengerBase + public class IProjectorScreenLiftControlMessenger : MessengerBase { private readonly IProjectorScreenLiftControl device; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs index 2579f056..a5f2cf3a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using System; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs index 88441526..187a82fb 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs @@ -1,13 +1,8 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Newtonsoft.Json.Converters; namespace PepperDash.Essentials.AppServer.Messengers { @@ -45,7 +40,7 @@ namespace PepperDash.Essentials.AppServer.Messengers foreach (var input in itemDevice.Items) { var key = input.Key; - var localItem = input.Value; + var localItem = input.Value; AddAction($"/{key}", (id, content) => { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs index 9b6f21d7..cdd238e2 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using PepperDash.Essentials.Core.CrestronIO; -using PepperDash.Essentials.Core.Shades; -using Newtonsoft.Json; +using Newtonsoft.Json; using PepperDash.Core; +using PepperDash.Essentials.Core.CrestronIO; +using System; namespace PepperDash.Essentials.AppServer.Messengers { @@ -41,7 +36,7 @@ namespace PepperDash.Essentials.AppServer.Messengers }); - device.OutputIsOnFeedback.OutputChange += new EventHandler((o, a) => SendFullStatus()); + device.OutputIsOnFeedback.OutputChange += new EventHandler((o, a) => SendFullStatus()); } private void SendFullStatus() diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs index 46e2a5a7..438fcef0 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Independentsoft.Json.Parser; -using Newtonsoft.Json; +using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Essentials.Core; @@ -82,8 +75,8 @@ namespace PepperDash.Essentials.AppServer.Messengers [JsonProperty("isValid", NullValueHandling = NullValueHandling.Ignore)] public bool? IsValid { get; set; } } - - class SetTechPasswordContent + + internal class SetTechPasswordContent { [JsonProperty("oldPassword")] public string OldPassword { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs index 3f8e36e5..2fd02a25 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json; using PepperDash.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.Lighting; using System; using System.Collections.Generic; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs index af9fbe43..7d809f13 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs @@ -1,5 +1,4 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core; @@ -81,9 +80,10 @@ namespace PepperDash.Essentials.AppServer.Messengers 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); + var route = path.Replace(MessagePath, string.Empty); - if(!_actions.TryGetValue(route, out var action)) { + if (!_actions.TryGetValue(route, out var action)) + { return; } @@ -136,12 +136,12 @@ namespace PepperDash.Essentials.AppServer.Messengers { try { - if(message == null) + if (message == null) { throw new ArgumentNullException("message"); } - if(_device == null) + if (_device == null) { throw new ArgumentNullException("device"); } @@ -154,7 +154,8 @@ namespace PepperDash.Essentials.AppServer.Messengers PostStatusMessage(JToken.FromObject(message), MessagePath, clientId); } - catch (Exception ex) { + catch (Exception ex) + { Debug.LogMessage(ex, "Exception posting status message", this); } } @@ -202,17 +203,17 @@ namespace PepperDash.Essentials.AppServer.Messengers { 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}", diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs index 2bf213ac..9ec2a4e4 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/PressAndHoldHandler.cs @@ -3,12 +3,11 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using System; using System.Collections.Generic; -using System.Runtime.CompilerServices; namespace PepperDash.Essentials.AppServer.Messengers { public static class PressAndHoldHandler - { + { private const long ButtonHeartbeatInterval = 1000; private static readonly Dictionary _pushedActions = new Dictionary(); @@ -54,7 +53,7 @@ namespace PepperDash.Essentials.AppServer.Messengers 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); @@ -70,7 +69,8 @@ namespace PepperDash.Essentials.AppServer.Messengers { Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Attempting to stop timer for {deviceKey}", deviceKey); - if (!_pushedActions.TryGetValue(deviceKey, out CTimer cancelTimer)) { + if (!_pushedActions.TryGetValue(deviceKey, out CTimer cancelTimer)) + { Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Timer for {deviceKey} not found", deviceKey); return; } @@ -110,7 +110,7 @@ namespace PepperDash.Essentials.AppServer.Messengers return; } - timerHandler(deviceKey, action); + timerHandler(deviceKey, action); } } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs index c316d13e..8b3b5681 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs @@ -1,7 +1,6 @@ using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Room.Config; using System; using System.Collections.Generic; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs index 1903b25f..3222289a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLAtcMessenger.cs @@ -1,7 +1,6 @@ using Crestron.SimplSharpPro.DeviceSupport; using Newtonsoft.Json.Linq; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Devices.Common.Codec; using System; using System.Collections.Generic; @@ -44,12 +43,12 @@ namespace PepperDash.Essentials.AppServer.Messengers 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" - }) + { + calls = GetCurrentCallList(), + currentCallString = _eisc.GetString(JoinMap.CurrentCallName.JoinNumber), + currentDialString = _eisc.GetString(JoinMap.CurrentDialString.JoinNumber), + isInCall = _eisc.GetString(JoinMap.HookState.JoinNumber) == "Connected" + }) ); } @@ -139,9 +138,9 @@ namespace PepperDash.Essentials.AppServer.Messengers private void SendCallsList() { PostStatusMessage(JToken.FromObject(new - { - calls = GetCurrentCallList(), - }) + { + calls = GetCurrentCallList(), + }) ); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs index de3f349d..bf4eb765 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLCameraMessenger.cs @@ -125,11 +125,11 @@ namespace PepperDash.Essentials.AppServer.Messengers } PostStatusMessage(JToken.FromObject(new - { - cameraMode = GetCameraMode(), - hasPresets = _eisc.GetBool(_joinMap.SupportsPresets.JoinNumber), - presets = presetList - }) + { + cameraMode = GetCameraMode(), + hasPresets = _eisc.GetBool(_joinMap.SupportsPresets.JoinNumber), + presets = presetList + }) ); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs index cf95b753..b0ddc47a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLDirectRouteMessenger.cs @@ -2,7 +2,6 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using System.Collections.Generic; namespace PepperDash.Essentials.AppServer.Messengers diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs index c1f6d78e..d6d59cc0 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLRouteMessenger.cs @@ -60,9 +60,9 @@ namespace PepperDash.Essentials.AppServer.Messengers sourceKey = "none"; PostStatusMessage(JToken.FromObject(new - { - selectedSourceKey = sourceKey - }) + { + selectedSourceKey = sourceKey + }) ); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs index e88acb12..a9872285 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SIMPLVtcMessenger.cs @@ -2,7 +2,6 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Devices.Common.Cameras; using PepperDash.Essentials.Devices.Common.Codec; using System; @@ -44,7 +43,7 @@ namespace PepperDash.Essentials.AppServer.Messengers /// /// protected override void RegisterActions() - { + { _eisc.SetStringSigAction(JoinMap.HookState.JoinNumber, s => { _currentCallItem.Status = (eCodecCallStatus)Enum.Parse(typeof(eCodecCallStatus), s, true); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs index 7abf0fca..c5f27318 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json; using PepperDash.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.Shades; using System; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs index 9c13c569..8b7750a2 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs @@ -2,7 +2,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.Monitoring; using System; using System.Threading.Tasks; @@ -71,15 +70,15 @@ namespace PepperDash.Essentials.AppServer.Messengers // This takes a while, launch a new thread Task.Run(() => PostStatusMessage(JToken.FromObject(new SystemMonitorStateMessage - { + { - TimeZone = systemMonitor.TimeZoneFeedback.IntValue, - TimeZoneName = systemMonitor.TimeZoneTextFeedback.StringValue, - IoControllerVersion = systemMonitor.IoControllerVersionFeedback.StringValue, - SnmpVersion = systemMonitor.SnmpVersionFeedback.StringValue, - BacnetVersion = systemMonitor.BaCnetAppVersionFeedback.StringValue, - ControllerVersion = systemMonitor.ControllerVersionFeedback.StringValue - }) + TimeZone = systemMonitor.TimeZoneFeedback.IntValue, + TimeZoneName = systemMonitor.TimeZoneTextFeedback.StringValue, + IoControllerVersion = systemMonitor.IoControllerVersionFeedback.StringValue, + SnmpVersion = systemMonitor.SnmpVersionFeedback.StringValue, + BacnetVersion = systemMonitor.BaCnetAppVersionFeedback.StringValue, + ControllerVersion = systemMonitor.ControllerVersionFeedback.StringValue + }) )); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs index ddd7e7ba..0fb5cbae 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs @@ -47,10 +47,10 @@ namespace PepperDash.Essentials.AppServer.Messengers private void CurrentInputFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) { PostStatusMessage(JToken.FromObject(new - { - currentInput = feedbackEventArgs.StringValue - }) - ); + { + currentInput = feedbackEventArgs.StringValue + }) + ); } @@ -66,21 +66,21 @@ namespace PepperDash.Essentials.AppServer.Messengers private void IsWarmingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) { PostStatusMessage(JToken.FromObject(new - { - isWarming = feedbackEventArgs.BoolValue - }) - ); + { + isWarming = feedbackEventArgs.BoolValue + }) + ); } private void IsCoolingFeedbackOnOutputChange(object sender, FeedbackEventArgs feedbackEventArgs) { PostStatusMessage(JToken.FromObject(new - { - isCooling = feedbackEventArgs.BoolValue - }) + { + isCooling = feedbackEventArgs.BoolValue + }) ); - + } #endregion diff --git a/src/PepperDash.Essentials.MobileControl/Interfaces.cs b/src/PepperDash.Essentials.MobileControl/Interfaces.cs index 6b455e84..dc5552c6 100644 --- a/src/PepperDash.Essentials.MobileControl/Interfaces.cs +++ b/src/PepperDash.Essentials.MobileControl/Interfaces.cs @@ -1,6 +1,6 @@ using System; -namespace PepperDash.Essentials.Room.MobileControl +namespace PepperDash.Essentials { /// /// Represents a room whose configuration is derived from runtime data, @@ -10,7 +10,7 @@ namespace PepperDash.Essentials.Room.MobileControl public interface IDelayedConfiguration { - + event EventHandler ConfigurationIsReady; } } \ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs index ac09fbc3..2f12f5bd 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlAction.cs @@ -1,11 +1,6 @@ using Newtonsoft.Json.Linq; using PepperDash.Essentials.Core.DeviceTypeInterfaces; -using PepperDash.Essentials.Core.Web.RequestHandlers; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Essentials { @@ -13,9 +8,10 @@ namespace PepperDash.Essentials { public IMobileControlMessenger Messenger { get; private set; } - public Action Action {get; private set; } + public Action Action { get; private set; } - public MobileControlAction(IMobileControlMessenger messenger, Action handler) { + public MobileControlAction(IMobileControlMessenger messenger, Action handler) + { Messenger = messenger; Action = handler; } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs index 4493e288..af25f27a 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs @@ -1,6 +1,5 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using System; using System.Collections.Generic; namespace PepperDash.Essentials @@ -57,7 +56,7 @@ namespace PepperDash.Essentials [JsonProperty("port")] public int Port { get; set; } - + } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs index 6ccbb382..0c1f1087 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs @@ -2,7 +2,6 @@ using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; -using PepperDash.Essentials.Room.MobileControl; using Serilog.Events; using System; using System.Collections.Generic; @@ -14,7 +13,7 @@ namespace PepperDash.Essentials public class MobileControlDeviceFactory : EssentialsDeviceFactory { public MobileControlDeviceFactory() - { + { TypeNames = new List { "appserver", "mobilecontrol", "webserver" }; } @@ -36,7 +35,7 @@ namespace PepperDash.Essentials public class MobileControlSimplFactory : EssentialsDeviceFactory { public MobileControlSimplFactory() - { + { TypeNames = new List { "mobilecontrolbridge-ddvc01", "mobilecontrolbridge-simpl" }; } @@ -55,7 +54,7 @@ namespace PepperDash.Essentials bridge.LogInformation("ERROR: Cannot connect bridge. System controller not present"); return; } - bridge.LogInformation("Linking to parent controller"); + bridge.LogInformation("Linking to parent controller"); parent.AddDeviceMessenger(bridge); }); diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs index e101dee8..58e4ed8a 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlFactory.cs @@ -1,24 +1,22 @@ using PepperDash.Core; using PepperDash.Essentials.Core; using System; -using System.Collections.Generic; using System.Linq; using System.Reflection; -using System.Text; -using System.Threading.Tasks; -namespace PepperDash.Essentials.MobileControl +namespace PepperDash.Essentials { public class MobileControlFactory { - public MobileControlFactory() { + public MobileControlFactory() + { var assembly = Assembly.GetExecutingAssembly(); PluginLoader.SetEssentialsAssembly(assembly.GetName().Name, assembly); var types = assembly.GetTypes().Where(t => typeof(IDeviceFactory).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); - if(types == null) + if (types == null) { return; } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 898fd10b..52555867 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -1,16 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Crestron.SimplSharp; +using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.Net.Http; using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Org.BouncyCastle.Crypto.Prng; using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer; @@ -32,9 +25,16 @@ using PepperDash.Essentials.Devices.Common.Displays; using PepperDash.Essentials.Devices.Common.SoftCodec; using PepperDash.Essentials.Devices.Common.VideoCodec; using PepperDash.Essentials.Room.MobileControl; +using PepperDash.Essentials.RoomBridges; using PepperDash.Essentials.Services; using PepperDash.Essentials.WebApiHandlers; -using Serilog.Events; +using PepperDash.Essentials.WebSocketServer; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Threading.Tasks; using WebSocketSharp; namespace PepperDash.Essentials @@ -160,7 +160,7 @@ namespace PepperDash.Essentials : base(key, name) { Config = config; - + // The queue that will collect the incoming messages in the order they are received //_receiveQueue = new ReceiveQueue(key, ParseStreamRx); _receiveQueue = new GenericQueue( @@ -222,7 +222,7 @@ namespace PepperDash.Essentials { _roomCombiner = DeviceManager.AllDevices.OfType().FirstOrDefault(); - if(_roomCombiner == null) + if (_roomCombiner == null) return; _roomCombiner.RoomCombinationScenarioChanged += OnRoomCombinationScenarioChanged; @@ -238,7 +238,7 @@ namespace PepperDash.Essentials return _wsClient2.IsAlive && IsAuthorized; }); - } + } private void SetupDefaultRoomMessengers() { @@ -247,7 +247,7 @@ namespace PepperDash.Essentials foreach (var room in DeviceManager.AllDevices.OfType()) { this.LogVerbose( - "Setting up room messengers for room: {key}", + "Setting up room messengers for room: {key}", room.Key ); @@ -260,7 +260,7 @@ namespace PepperDash.Essentials AddDefaultDeviceMessenger(messenger); this.LogVerbose( - "Attempting to set up default room messengers for room: {0}", + "Attempting to set up default room messengers for room: {0}", room.Key ); @@ -328,7 +328,7 @@ namespace PepperDash.Essentials var allDevices = DeviceManager.AllDevices.Where((d) => !(d is IEssentialsRoom)); this.LogInformation( - "All Devices that aren't rooms count: {0}", + "All Devices that aren't rooms count: {0}", allDevices?.Count() ); @@ -425,26 +425,26 @@ namespace PepperDash.Essentials var presetsDevice = device as ITvPresetsProvider; - this.LogVerbose( - "Adding ITvPresetsProvider for {deviceKey}", - device.Key - ); + this.LogVerbose( + "Adding ITvPresetsProvider for {deviceKey}", + device.Key + ); - var presetsMessenger = new DevicePresetsModelMessenger( - $"{device.Key}-presets-{Key}", - $"/device/{device.Key}", - presetsDevice - ); + var presetsMessenger = new DevicePresetsModelMessenger( + $"{device.Key}-presets-{Key}", + $"/device/{device.Key}", + presetsDevice + ); - AddDefaultDeviceMessenger(presetsMessenger); + AddDefaultDeviceMessenger(presetsMessenger); + + messengerAdded = true; + } - messengerAdded = true; - } - if (device is DisplayBase) { - this.LogVerbose( "Adding actions for device: {0}", device.Key); + this.LogVerbose("Adding actions for device: {0}", device.Key); var dbMessenger = new DisplayBaseMessenger( $"{device.Key}-displayBase-{Key}", @@ -458,7 +458,7 @@ namespace PepperDash.Essentials } if (device is TwoWayDisplayBase twoWayDisplay) - { + { this.LogVerbose( "Adding TwoWayDisplayBase for {deviceKey}", device.Key @@ -476,11 +476,11 @@ namespace PepperDash.Essentials if (device is IBasicVolumeWithFeedback) { var deviceKey = device.Key; - this.LogVerbose( - "Adding IBasicVolumeControlWithFeedback for {deviceKey}", - deviceKey - ); - + this.LogVerbose( + "Adding IBasicVolumeControlWithFeedback for {deviceKey}", + deviceKey + ); + var volControlDevice = device as IBasicVolumeWithFeedback; var messenger = new DeviceVolumeMessenger( $"{device.Key}-volume-{Key}", @@ -500,7 +500,7 @@ namespace PepperDash.Essentials "Adding LightingBaseMessenger for {deviceKey}", deviceKey ); - + var lightingDevice = device as ILightingScenes; var messenger = new ILightingScenesMessenger( $"{device.Key}-lighting-{Key}", @@ -516,7 +516,7 @@ namespace PepperDash.Essentials { var deviceKey = device.Key; var shadeDevice = device as IShadesOpenCloseStop; - + this.LogVerbose( "Adding ShadeBaseMessenger for {deviceKey}", deviceKey @@ -535,7 +535,7 @@ namespace PepperDash.Essentials if (device is VideoCodecBase codec) { this.LogVerbose( - "Adding VideoCodecBaseMessenger for {deviceKey}", codec.Key ); + "Adding VideoCodecBaseMessenger for {deviceKey}", codec.Key); var messenger = new VideoCodecBaseMessenger( $"{codec.Key}-videoCodec-{Key}", @@ -571,7 +571,7 @@ namespace PepperDash.Essentials "Adding ISetTopBoxControlMessenger for {deviceKey}" ); - var dev = device as Device; + var dev = device as Device; var messenger = new ISetTopBoxControlsMessenger( $"{device.Key}-stb-{Key}", @@ -639,7 +639,7 @@ namespace PepperDash.Essentials if (device is INumericKeypad) { - this.LogVerbose("Adding INumericKeyapdMessenger for {deviceKey}", device.Key); + this.LogVerbose("Adding INumericKeyapdMessenger for {deviceKey}", device.Key); var dev = device as PepperDash.Core.Device; @@ -709,7 +709,7 @@ namespace PepperDash.Essentials if (device is IHasCurrentSourceInfoChange) { - this.LogVerbose("Adding IHasCurrentSourceInfoMessenger for {deviceKey}", device.Key ); + this.LogVerbose("Adding IHasCurrentSourceInfoMessenger for {deviceKey}", device.Key); var messenger = new IHasCurrentSourceInfoMessenger( $"{device.Key}-currentSource-{Key}", @@ -942,7 +942,7 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is IDspPresets dspPresets) + if (device is IDspPresets dspPresets) { this.LogVerbose("Adding IDspPresetsMessenger for {deviceKey}", device.Key ); @@ -958,13 +958,11 @@ namespace PepperDash.Essentials messengerAdded = true; } - this.LogVerbose("Trying to cast to generic device for device: {key}", device.Key ); + this.LogVerbose("Trying to cast to generic device for device: {key}", device.Key); if (device is EssentialsDevice) { - var genericDevice = device as EssentialsDevice; - - if (genericDevice == null || messengerAdded) + if (!(device is EssentialsDevice genericDevice) || messengerAdded) { this.LogVerbose( "Skipping GenericMessenger for {deviceKey}. Messenger(s) Added: {messengersAdded}.", @@ -1100,7 +1098,7 @@ namespace PepperDash.Essentials $"Connecting to MC API server" ); - ConnectWebsocketClient(); + ConnectWebsocketClient(); }, "mobileconnect", "Forces connect of websocket", @@ -1111,7 +1109,7 @@ namespace PepperDash.Essentials s => { _disableReconnect = true; - + CleanUpWebsocketClient(); CrestronConsole.ConsoleCommandResponse( @@ -1334,7 +1332,7 @@ namespace PepperDash.Essentials { if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Appliance) { - this.LogWarning( + this.LogWarning( "System Monitor does not exist for this platform. Skipping..." ); return; @@ -1394,7 +1392,7 @@ namespace PepperDash.Essentials CrestronConsole.ConsoleCommandResponse( $"{cmdparameters} is not a valid debug level. Valid options are:\r\n{LogLevel.Trace}\r\n{LogLevel.Debug}\r\n{LogLevel.Info}\r\n{LogLevel.Warn}\r\n{LogLevel.Error}\r\n{LogLevel.Fatal}\r\n" ); - + } } @@ -1617,8 +1615,8 @@ namespace PepperDash.Essentials if (Config.EnableApiServer) { - CrestronConsole.ConsoleCommandResponse( - @"Mobile Control Edge Server API Information: + CrestronConsole.ConsoleCommandResponse( + @"Mobile Control Edge Server API Information: Server address: {0} System Name: {1} @@ -1627,14 +1625,14 @@ namespace PepperDash.Essentials System User code: {4} Connected?: {5} Seconds Since Last Ack: {6}", - url, - name, - ConfigReader.ConfigObject.SystemUrl, - SystemUuid, - code, - conn, - secSinceLastAck.Seconds - ); + url, + name, + ConfigReader.ConfigObject.SystemUrl, + SystemUuid, + code, + conn, + secSinceLastAck.Seconds + ); } else { @@ -1960,17 +1958,16 @@ Mobile Control Direct Server Infromation: confObject.Info.RuntimeInfo.AssemblyVersion = essentialsVersion; -// // Set for local testing -// confObject.RuntimeInfo.PluginVersion = "4.0.0-localBuild"; + // // Set for local testing + // confObject.RuntimeInfo.PluginVersion = "4.0.0-localBuild"; // Populate the plugin version var pluginVersion = Assembly .GetExecutingAssembly() .GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); - var fullVersionAtt = pluginVersion[0] as AssemblyInformationalVersionAttribute; - if (fullVersionAtt != null) + if (pluginVersion[0] is AssemblyInformationalVersionAttribute fullVersionAtt) { var pluginInformationalVersion = fullVersionAtt.InformationalVersion; @@ -2003,7 +2000,7 @@ Mobile Control Direct Server Infromation: if (Config.EnableApiServer) { - _transmitToServerQueue.Enqueue(new TransmitMessage(o, _wsClient2)); + _transmitToServerQueue.Enqueue(new TransmitMessage(o, _wsClient2)); } @@ -2074,7 +2071,7 @@ Mobile Control Direct Server Infromation: private void PingTimerCallback(object o) { this.LogDebug( - + "Ping timer expired. Closing websocket" ); @@ -2142,7 +2139,6 @@ Mobile Control Direct Server Infromation: { var clientId = content["clientId"].Value(); var roomKey = content["roomKey"].Value(); - var touchpanelKey = content.SelectToken("touchpanelKey"); //content["touchpanelKey"].Value(); if (_roomCombiner == null) { @@ -2198,7 +2194,7 @@ Mobile Control Direct Server Infromation: catch { qrChecksum = new JValue(string.Empty); - } + } if (code == null) { @@ -2283,7 +2279,7 @@ Mobile Control Direct Server Infromation: // Can't do direct comparison because it will match /room/roomA with /room/roomA/xxx instead of /room/roomAB/xxx var handlersKv = _actionDictionary.FirstOrDefault(kv => message.Type.StartsWith(kv.Key + "/")); // adds trailing slash to ensure above case is handled - + if (handlersKv.Key == null) { diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index ca694e96..c32f226f 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -30,6 +30,11 @@ + + + + + @@ -48,15 +53,15 @@ - + false runtime - + false runtime - + false runtime diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs index e46eaf34..c005ca17 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlBridgeBase.cs @@ -1,10 +1,11 @@ using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using System; -namespace PepperDash.Essentials +namespace PepperDash.Essentials.RoomBridges { /// /// @@ -102,12 +103,12 @@ namespace PepperDash.Essentials /// protected virtual void UserCodeChange() { - Debug.Console(1, this, "Server user code changed: {0}", UserCode); + this.LogDebug("Server user code changed: {userCode}", UserCode); var qrUrl = string.Format($"{Parent.Host}/api/rooms/{Parent.SystemUuid}/{RoomKey}/qr?x={new Random().Next()}"); QrCodeUrl = qrUrl; - Debug.Console(1, this, "Server user code changed: {0} - {1}", UserCode, qrUrl); + this.LogDebug("Server user code changed: {userCode} - {qrCodeUrl}", UserCode, qrUrl); OnUserCodeChanged(); } diff --git a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs index ba6a3b0b..ac53a1d4 100644 --- a/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs +++ b/src/PepperDash.Essentials.MobileControl/RoomBridges/MobileControlEssentialsRoomBridge.cs @@ -16,14 +16,14 @@ using PepperDash.Essentials.Devices.Common.Cameras; using PepperDash.Essentials.Devices.Common.Room; using PepperDash.Essentials.Devices.Common.VideoCodec; using PepperDash.Essentials.Room.Config; +using PepperDash.Essentials.WebSocketServer; using System; using System.Collections.Generic; using System.Linq; using IShades = PepperDash.Essentials.Core.Shades.IShades; using ShadeBase = PepperDash.Essentials.Devices.Common.Shades.ShadeBase; -using Volume = PepperDash.Essentials.Room.MobileControl.Volume; -namespace PepperDash.Essentials +namespace PepperDash.Essentials.RoomBridges { public class MobileControlEssentialsRoomBridge : MobileControlBridgeBase { @@ -339,7 +339,7 @@ namespace PepperDash.Essentials string shareText; bool isSharing; - if (Room is IHasCurrentSourceInfoChange srcInfoRoom && (Room is IHasVideoCodec vcRoom && (vcRoom.VideoCodec.SharingContentIsOnFeedback.BoolValue && srcInfoRoom.CurrentSourceInfo != null))) + if (Room is IHasCurrentSourceInfoChange srcInfoRoom && Room is IHasVideoCodec vcRoom && vcRoom.VideoCodec.SharingContentIsOnFeedback.BoolValue && srcInfoRoom.CurrentSourceInfo != null) { shareText = srcInfoRoom.CurrentSourceInfo.PreferredName; isSharing = true; @@ -532,7 +532,8 @@ namespace PepperDash.Essentials } return state; - } catch (Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Error getting full status", this); return null; @@ -560,7 +561,7 @@ namespace PepperDash.Essentials { var zrcTp = DeviceManager.AllDevices.OfType().SingleOrDefault((tp) => tp.ZoomRoomController); - configuration.ZoomRoomControllerKey = zrcTp != null ? zrcTp.Key : null; + configuration.ZoomRoomControllerKey = zrcTp?.Key; } catch { @@ -578,7 +579,7 @@ namespace PepperDash.Essentials // find the room combiner for this room by checking if the room is in the list of rooms for the room combiner var roomCombiner = DeviceManager.AllDevices.OfType().FirstOrDefault(); - configuration.RoomCombinerKey = roomCombiner != null ? roomCombiner.Key : null; + configuration.RoomCombinerKey = roomCombiner?.Key; if (room is IEssentialsRoomPropertiesConfig propertiesConfig) @@ -620,7 +621,8 @@ namespace PepperDash.Essentials configuration.VideoCodecKey = vcRoom.VideoCodec.Key; configuration.VideoCodecIsZoomRoom = type.Name.Equals("ZoomRoom", StringComparison.InvariantCultureIgnoreCase); } - }; + } + ; if (room is IHasAudioCodec acRoom) { @@ -655,7 +657,7 @@ namespace PepperDash.Essentials eEnvironmentalDeviceTypes type = eEnvironmentalDeviceTypes.None; - if (dev is ILightingScenes || dev is Devices.Common.Lighting.LightingBase) + if (dev is ILightingScenes) { type = eEnvironmentalDeviceTypes.Lighting; } @@ -871,7 +873,7 @@ namespace PepperDash.Essentials public Dictionary SourceList { get; set; } [JsonProperty("destinationList", NullValueHandling = NullValueHandling.Ignore)] - public Dictionary DestinationList { get; set;} + public Dictionary DestinationList { get; set; } [JsonProperty("audioControlPointList", NullValueHandling = NullValueHandling.Ignore)] public AudioControlPointListItem AudioControlPointList { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs index 00ae00b0..262c7e07 100644 --- a/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs +++ b/src/PepperDash.Essentials.MobileControl/Services/MobileControlApiService.cs @@ -66,7 +66,8 @@ namespace PepperDash.Essentials.Services } return authResponse; - } catch(Exception ex) + } + catch (Exception ex) { Debug.LogMessage(ex, "Error authorizing with Mobile Control"); return new AuthorizationResponse { Authorized = false, Reason = ex.Message }; diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs index 3d6eb7b9..7dc4ae16 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITheme.cs @@ -1,15 +1,9 @@ using PepperDash.Core; -using PepperDash.Essentials.Core; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace PepperDash.Essentials.Touchpanel { - public interface ITheme:IKeyed - { + public interface ITheme : IKeyed + { string Theme { get; } void UpdateTheme(string theme); diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs index dd69d2da..19e04775 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswAppControlMessenger.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; namespace PepperDash.Essentials.Touchpanel { diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs index 66ed4c1f..cbd4f6a2 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ITswZoomControlMessenger.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; -using PepperDash.Essentials.Core.DeviceTypeInterfaces; namespace PepperDash.Essentials.Touchpanel @@ -41,7 +40,7 @@ namespace PepperDash.Essentials.Touchpanel _zoomControl.ZoomInCallFeedback.OutputChange += (s, a) => - { + { PostStatusMessage(JToken.FromObject( new { diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs index 703cb291..4f5078a3 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelController.cs @@ -10,13 +10,12 @@ using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Core.DeviceInfo; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.UI; -using PepperDash.Essentials.Touchpanel; using System; using System.Collections.Generic; using System.Linq; using Feedback = PepperDash.Essentials.Core.Feedback; -namespace PepperDash.Essentials.Devices.Common.TouchPanel +namespace PepperDash.Essentials.Touchpanel { //public interface IMobileControlTouchpanelController //{ @@ -75,7 +74,7 @@ namespace PepperDash.Essentials.Devices.Common.TouchPanel AddPostActivationAction(SubscribeForMobileControlUpdates); - ThemeFeedback = new StringFeedback($"{Key}-theme",() => Theme); + ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme); AppUrlFeedback = new StringFeedback($"{Key}-appUrl", () => _appUrl); QrCodeUrlFeedback = new StringFeedback($"{Key}-qrCodeUrl", () => _bridge?.QrCodeUrl); McServerUrlFeedback = new StringFeedback($"{Key}-mcServerUrl", () => _bridge?.McServerUrl); @@ -357,10 +356,11 @@ namespace PepperDash.Essentials.Devices.Common.TouchPanel _bridge = bridge; _bridge.UserCodeChanged += UpdateFeedbacks; - _bridge.AppUrlChanged += (s, a) => { - this.LogInformation("AppURL changed"); + _bridge.AppUrlChanged += (s, a) => + { + this.LogInformation("AppURL changed"); SetAppUrl(_bridge.AppUrl); - UpdateFeedbacks(s, a); + UpdateFeedbacks(s, a); }; SetAppUrl(_bridge.AppUrl); diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs index 1d8f8ebf..87f6d9a9 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/MobileControlTouchpanelProperties.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Devices.Common.TouchPanel +namespace PepperDash.Essentials.Touchpanel { public class MobileControlTouchpanelProperties : CrestronTouchpanelPropertiesConfig { diff --git a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs index cf93197c..ab48b60a 100644 --- a/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl/Touchpanel/ThemeMessenger.cs @@ -27,14 +27,14 @@ namespace PepperDash.Essentials.Touchpanel var theme = content.ToObject>(); Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Setting theme to {theme}", this, theme.Value); - _tpDevice.UpdateTheme(theme.Value); + _tpDevice.UpdateTheme(theme.Value); - PostStatusMessage(JToken.FromObject(new {theme = theme.Value})); + PostStatusMessage(JToken.FromObject(new { theme = theme.Value })); }); } } - public class ThemeUpdateMessage:DeviceStateMessageBase + public class ThemeUpdateMessage : DeviceStateMessageBase { [JsonProperty("theme")] public string Theme { get; set; } diff --git a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs index 6e536f4a..14f928ca 100644 --- a/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs +++ b/src/PepperDash.Essentials.MobileControl/TransmitMessage.cs @@ -4,6 +4,7 @@ using PepperDash.Core; using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core.Queues; +using PepperDash.Essentials.WebSocketServer; using Serilog.Events; using System; using System.Threading; @@ -46,19 +47,19 @@ namespace PepperDash.Essentials return; } - + var message = JsonConvert.SerializeObject(msgToSend, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore, Converters = { new IsoDateTimeConverter() } }); Debug.LogMessage(LogEventLevel.Verbose, "Message TX: {0}", null, message); _ws.Send(message); - - + + } catch (Exception ex) { - Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); + Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); } } #endregion @@ -89,7 +90,7 @@ namespace PepperDash.Essentials { try { - if(_server == null) + if (_server == null) { Debug.LogMessage(LogEventLevel.Warning, "Cannot send message. Server is null"); return; @@ -109,13 +110,13 @@ namespace PepperDash.Essentials return; } - - _server.SendMessageToAllClients(message); - _server.LogVerbose("Message TX To all clients: {message}", null, message); - + _server.SendMessageToAllClients(message); + + _server.LogVerbose("Message TX To all clients: {message}", null, message); + + - } catch (ThreadAbortException) { @@ -123,7 +124,7 @@ namespace PepperDash.Essentials } catch (Exception ex) { - Debug.LogMessage(ex,"Caught an exception in the Transmit Processor"); + Debug.LogMessage(ex, "Caught an exception in the Transmit Processor"); } diff --git a/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs b/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs index ef004421..851f1b80 100644 --- a/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs +++ b/src/PepperDash.Essentials.MobileControl/UserCodeChangedContent.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -namespace PepperDash.Essentials.AppServer +namespace PepperDash.Essentials { public class UserCodeChangedContent { diff --git a/src/PepperDash.Essentials.MobileControl/Volumes.cs b/src/PepperDash.Essentials.MobileControl/Volumes.cs index 88556675..6cd83cf4 100644 --- a/src/PepperDash.Essentials.MobileControl/Volumes.cs +++ b/src/PepperDash.Essentials.MobileControl/Volumes.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using System.Collections.Generic; -namespace PepperDash.Essentials.Room.MobileControl +namespace PepperDash.Essentials { public class Volumes { diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs index 7351cc1e..2988241b 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/ActionPathsHandler.cs @@ -1,7 +1,6 @@ using Crestron.SimplSharp.WebScripting; using Newtonsoft.Json; using PepperDash.Core.Web.RequestHandlers; -using PepperDash.Essentials.AppServer.Messengers; using System.Collections.Generic; using System.Linq; @@ -33,7 +32,7 @@ namespace PepperDash.Essentials.WebApiHandlers private readonly MobileControlSystemController mcController; [JsonProperty("actionPaths")] - public List ActionPaths => mcController.GetActionDictionaryPaths().Select((path) => new ActionPath { MessengerKey = path.Item1, Path = path.Item2}).ToList(); + public List ActionPaths => mcController.GetActionDictionaryPaths().Select((path) => new ActionPath { MessengerKey = path.Item1, Path = path.Item2 }).ToList(); public ActionPathsResponse(MobileControlSystemController mcController) { diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs index f0c1809c..c8b97d0b 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/MobileInfoHandler.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Config; +using PepperDash.Essentials.WebSocketServer; using System; using System.Collections.Generic; using System.Linq; @@ -30,8 +31,7 @@ namespace PepperDash.Essentials.WebApiHandlers } catch (Exception ex) { - Debug.Console(1, $"exception showing mobile info: {ex.Message}"); - Debug.Console(2, $"stack trace: {ex.StackTrace}"); + Debug.LogMessage(ex, "exception showing mobile info"); context.Response.StatusCode = 500; context.Response.End(); @@ -143,7 +143,7 @@ namespace PepperDash.Essentials.WebApiHandlers public string Token => Key; [JsonProperty("connected")] - public bool Connected => context.Client == null ? false : context.Client.Context.WebSocket.IsAlive; + public bool Connected => context.Client != null && context.Client.Context.WebSocket.IsAlive; [JsonProperty("duration")] public double Duration => context.Client == null ? 0 : context.Client.ConnectedDuration.TotalSeconds; diff --git a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs index 56d61347..73fdb104 100644 --- a/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs +++ b/src/PepperDash.Essentials.MobileControl/WebApiHandlers/UiClientHandler.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using PepperDash.Core; using PepperDash.Core.Web.RequestHandlers; using PepperDash.Essentials.Core.Web; +using PepperDash.Essentials.WebSocketServer; using Serilog.Events; namespace PepperDash.Essentials.WebApiHandlers diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs index ee9ff1f3..8cf96cc5 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/MobileControlWebsocketServer.cs @@ -8,6 +8,7 @@ using PepperDash.Essentials.AppServer.Messengers; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Core.Web; +using PepperDash.Essentials.RoomBridges; using PepperDash.Essentials.WebApiHandlers; using Serilog.Events; using System; @@ -23,7 +24,7 @@ using WebSocketSharp.Server; using ErrorEventArgs = WebSocketSharp.ErrorEventArgs; -namespace PepperDash.Essentials +namespace PepperDash.Essentials.WebSocketServer { /// /// Represents the behaviour to associate with a UiClient for WebSocket communication @@ -255,14 +256,14 @@ namespace PepperDash.Essentials Port = customPort; } - if(parent.Config.DirectServer.AutomaticallyForwardPortToCSLAN == true) + if (parent.Config.DirectServer.AutomaticallyForwardPortToCSLAN == true) { try { CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter); - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Automatically forwarding port {0} to CS LAN", Port); + Debug.LogMessage(LogEventLevel.Information, "Automatically forwarding port {0} to CS LAN", Port); var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(EthernetAdapterType.EthernetCSAdapter); var csIp = CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId); @@ -271,12 +272,12 @@ namespace PepperDash.Essentials if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Error adding port forwarding: {0}", result); + Debug.LogMessage(LogEventLevel.Error, "Error adding port forwarding: {0}", result); } } catch (ArgumentException) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "This processor does not have a CS LAN", this); + Debug.LogMessage(LogEventLevel.Information, "This processor does not have a CS LAN", this); } catch (Exception ex) { @@ -351,7 +352,7 @@ namespace PepperDash.Essentials if (_server.IsListening) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Mobile Control WebSocket Server listening on port {port}", this, _server.Port); + Debug.LogMessage(LogEventLevel.Information, "Mobile Control WebSocket Server listening on port {port}", this, _server.Port); } CrestronEnvironment.ProgramStatusEventHandler += OnProgramStop; @@ -537,7 +538,7 @@ namespace PepperDash.Essentials { Debug.LogMessage(ex, "Error getting application configuration", this); - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Config Object: {config} from {parentConfig}", this, config, _parent.Config); + Debug.LogMessage(LogEventLevel.Verbose, "Config Object: {config} from {parentConfig}", this, config, _parent.Config); } return config; @@ -558,9 +559,9 @@ namespace PepperDash.Essentials if (secret != null) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Secret successfully retrieved", this); + Debug.LogMessage(LogEventLevel.Information, "Secret successfully retrieved", this); - Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Secret: {0}", this, secret.Value.ToString()); + Debug.LogMessage(LogEventLevel.Debug, "Secret: {0}", this, secret.Value.ToString()); // populate the local secrets object @@ -571,27 +572,27 @@ namespace PepperDash.Essentials // populate the _uiClient collection foreach (var token in _secret.Tokens) { - if(token.Value == null) + if (token.Value == null) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "Token value is null", this); + Debug.LogMessage(LogEventLevel.Warning, "Token value is null", this); continue; - } + } - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Adding token: {0} for room: {1}", this, token.Key, token.Value.RoomKey); - - if(UiClients == null) + Debug.LogMessage(LogEventLevel.Information, "Adding token: {0} for room: {1}", this, token.Key, token.Value.RoomKey); + + if (UiClients == null) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "UiClients is null", this); + Debug.LogMessage(LogEventLevel.Warning, "UiClients is null", this); UiClients = new Dictionary(); } - + UiClients.Add(token.Key, new UiClientContext(token.Value)); } } if (UiClients.Count > 0) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Restored {uiClientCount} UiClients from secrets data", this, UiClients.Count); + Debug.LogMessage(LogEventLevel.Information, "Restored {uiClientCount} UiClients from secrets data", this, UiClients.Count); foreach (var client in UiClients) { @@ -602,7 +603,7 @@ namespace PepperDash.Essentials _server.AddWebSocketService(path, () => { var c = new UiClient(); - Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "Constructing UiClient with id: {key}", this, key); + Debug.LogMessage(LogEventLevel.Debug, "Constructing UiClient with id: {key}", this, key); c.Controller = _parent; c.RoomKey = roomKey; @@ -623,10 +624,10 @@ namespace PepperDash.Essentials } else { - Debug.LogMessage(Serilog.Events.LogEventLevel.Warning, "No secret found"); + Debug.LogMessage(LogEventLevel.Warning, "No secret found"); } - Debug.LogMessage(Serilog.Events.LogEventLevel.Debug, "{uiClientCount} UiClients restored from secrets data", this, UiClients.Count); + Debug.LogMessage(LogEventLevel.Debug, "{uiClientCount} UiClients restored from secrets data", this, UiClients.Count); } catch (Exception ex) { @@ -643,7 +644,7 @@ namespace PepperDash.Essentials { if (_secret == null) { - Debug.LogMessage(Serilog.Events.LogEventLevel.Error, "Secret is null", this); + Debug.LogMessage(LogEventLevel.Error, "Secret is null", this); _secret = new ServerTokenSecrets(string.Empty); } @@ -748,17 +749,17 @@ namespace PepperDash.Essentials _server.AddWebSocketService(path, () => { var c = new UiClient(); - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Constructing UiClient with id: {0}", this, key); + Debug.LogMessage(LogEventLevel.Verbose, "Constructing UiClient with id: {0}", this, key); c.Controller = _parent; c.RoomKey = bridge.RoomKey; UiClients[key].SetClient(c); return c; }); - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Added new WebSocket UiClient service at path: {path}", this, path); - Debug.LogMessage(Serilog.Events.LogEventLevel.Information, "Token: {@token}", this, token); + Debug.LogMessage(LogEventLevel.Information, "Added new WebSocket UiClient service at path: {path}", this, path); + Debug.LogMessage(LogEventLevel.Information, "Token: {@token}", this, token); - Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "{serviceCount} websocket services present", this, _server.WebSocketServices.Count); + Debug.LogMessage(LogEventLevel.Verbose, "{serviceCount} websocket services present", this, _server.WebSocketServices.Count); UpdateSecret(); @@ -970,7 +971,7 @@ namespace PepperDash.Essentials } catch (Exception ex) { - this.LogException(ex, "Caught an exception in the OnPost handler"); + this.LogException(ex, "Caught an exception in the OnPost handler"); } } @@ -1048,7 +1049,7 @@ namespace PepperDash.Essentials var body = Encoding.UTF8.GetBytes(message); res.ContentLength64 = body.LongLength; res.Close(body, true); - + } } else @@ -1101,23 +1102,23 @@ namespace PepperDash.Essentials if (File.Exists(filePath)) { - if(filePath.EndsWith(".png")) - { + if (filePath.EndsWith(".png")) + { res.ContentType = "image/png"; } - else if(filePath.EndsWith(".jpg")) + else if (filePath.EndsWith(".jpg")) { res.ContentType = "image/jpeg"; } - else if(filePath.EndsWith(".gif")) + else if (filePath.EndsWith(".gif")) { res.ContentType = "image/gif"; } - else if(filePath.EndsWith(".svg")) + else if (filePath.EndsWith(".svg")) { res.ContentType = "image/svg+xml"; } - byte[] contents = System.IO.File.ReadAllBytes(filePath); + byte[] contents = File.ReadAllBytes(filePath); res.ContentLength64 = contents.LongLength; res.Close(contents, true); } @@ -1126,7 +1127,7 @@ namespace PepperDash.Essentials res.StatusCode = (int)HttpStatusCode.NotFound; res.Close(); } - } + } /// /// Handles requests to serve files for the Angular single page app @@ -1138,9 +1139,6 @@ namespace PepperDash.Essentials { this.LogVerbose("Requesting User app file"); - var qp = req.QueryString; - var token = qp["token"]; - string filePath = path.Split('?')[0]; // remove the token from the path if found @@ -1202,10 +1200,10 @@ namespace PepperDash.Essentials this.LogVerbose("Attempting to serve file: {filePath}", filePath); byte[] contents; - if (System.IO.File.Exists(filePath)) + if (File.Exists(filePath)) { this.LogVerbose("File found: {filePath}", filePath); - contents = System.IO.File.ReadAllBytes(filePath); + contents = File.ReadAllBytes(filePath); } else { diff --git a/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs index 7aa40996..1b797767 100644 --- a/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs +++ b/src/PepperDash.Essentials.MobileControl/WebSocketServer/WebSocketServerSecretProvider.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials +namespace PepperDash.Essentials.WebSocketServer { internal class WebSocketServerSecretProvider : CrestronLocalSecretsProvider { From 277771d15470623cdeda3285c9a65f0ff6c7ba19 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 22:46:41 -0500 Subject: [PATCH 17/26] chore: miscellaeneous cleanup --- .../Comm and IR/GenericHttpClient.cs | 19 +- .../Devices/PC/Laptop.cs | 81 ----- ...lsHuddleSpaceFusionSystemControllerBase.cs | 90 ++---- .../Touchpanels/Mpc3Touchpanel.cs | 113 ++++--- .../SetDeviceStreamDebugRequestHandler.cs | 21 +- .../Cameras/CameraVisca.cs | 39 ++- .../Codec/iHasScheduleAwareness.cs | 20 +- .../VideoCodec/VideoCodecBase.cs | 288 ++++++++---------- .../DisplayBaseMessenger.cs | 3 +- .../Messengers/IDspPresetsMessenger.cs | 10 +- .../IEssentialsRoomCombinerMessenger.cs | 3 +- .../Messengers/IRunRouteActionMessenger.cs | 2 +- .../Messengers/LightingBaseMessenger.cs | 2 - .../Messengers/RoomEventScheduleMessenger.cs | 3 +- .../Messengers/SystemMonitorMessenger.cs | 6 +- .../Messengers/VideoCodecBaseMessenger.cs | 21 +- ...Essentials.MobileControl.Messengers.csproj | 12 +- .../MobileControlDeviceFactory.cs | 50 --- src/PepperDash.Essentials/ControlSystem.cs | 2 +- 19 files changed, 289 insertions(+), 496 deletions(-) delete mode 100644 src/PepperDash.Essentials.Core/Devices/PC/Laptop.cs 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/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/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs index ab04a0b0..9a2667f0 100644 --- a/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs +++ b/src/PepperDash.Essentials.Core/Fusion/EssentialsHuddleSpaceFusionSystemControllerBase.cs @@ -20,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; @@ -30,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; @@ -86,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 @@ -152,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) { @@ -368,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; } @@ -378,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); } @@ -661,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); } @@ -902,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); } @@ -1015,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 }); } } } @@ -1092,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) { @@ -1124,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; } @@ -1169,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); } @@ -1214,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) { @@ -1232,7 +1215,7 @@ namespace PepperDash.Essentials.Core.Fusion } else { - attrNum = attrNum + xpanelNum; + attrNum += xpanelNum; if (attrNum > JoinMap.TouchpanelOnlineStart.JoinSpan) { @@ -1248,7 +1231,7 @@ namespace PepperDash.Essentials.Core.Fusion //else if (dev is IDisplay) { - attrNum = attrNum + displayNum; + attrNum += displayNum; if (attrNum > JoinMap.DisplayOnlineStart.JoinSpan) { continue; @@ -1300,13 +1283,11 @@ namespace PepperDash.Essentials.Core.Fusion display.UsageTracker.DeviceUsageEnded += UsageTracker_DeviceUsageEnded; } - var hasDefaultDisplay = Room as IHasDefaultDisplay; - if (hasDefaultDisplay == null) + if (!(Room is IHasDefaultDisplay hasDefaultDisplay)) { return; } - var defaultDisplay = hasDefaultDisplay.DefaultDisplay as IDisplay; - 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; @@ -1358,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) @@ -1392,8 +1372,7 @@ namespace PepperDash.Essentials.Core.Fusion 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; } @@ -1402,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; } @@ -1436,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); @@ -1450,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); } @@ -1465,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", @@ -1532,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; @@ -1601,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) @@ -1760,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/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/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/DeviceTypeExtensions/DisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs index 3d15db76..cc09a637 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/DisplayBaseMessenger.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.AppServer; using PepperDash.Essentials.AppServer.Messengers; using System.Linq; @@ -32,7 +33,7 @@ namespace PepperDash.Essentials.Room.MobileControl if (inputPort == null) { - Debug.Console(1, "No input named {0} found for device {1}", s, display.Key); + this.LogWarning("No input named {inputName} found for {deviceKey}", s, display.Key); return; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs index 31529566..e40cd8eb 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IDspPresetsMessenger.cs @@ -7,12 +7,12 @@ namespace PepperDash.Essentials.AppServer.Messengers { public class IDspPresetsMessenger : MessengerBase { - private IDspPresets _device; + private readonly IDspPresets device; public IDspPresetsMessenger(string key, string messagePath, IDspPresets device) - : base(key, messagePath, device as Device) + : base(key, messagePath, device as IKeyName) { - _device = device; + this.device = device; } protected override void RegisterActions() @@ -23,7 +23,7 @@ namespace PepperDash.Essentials.AppServer.Messengers { var message = new IHasDspPresetsStateMessage { - Presets = _device.Presets + Presets = device.Presets }; PostStatusMessage(message); @@ -36,7 +36,7 @@ namespace PepperDash.Essentials.AppServer.Messengers if (!string.IsNullOrEmpty(presetKey)) { - _device.RecallPreset(presetKey); + device.RecallPreset(presetKey); } }); } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs index 2cedf5fa..9dc9eac7 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using System; using System.Collections.Generic; @@ -117,7 +118,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } catch (Exception e) { - Debug.Console(0, this, "Error sending full status: {0}", e); + this.LogException(e, "Error sending full status"); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs index a5f2cf3a..98824530 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using System; @@ -43,7 +44,6 @@ namespace PepperDash.Essentials.AppServer.Messengers if (!string.IsNullOrEmpty(c.SourceListKey)) { // Check for source list in content of message - Debug.Console(1, this, "sourceListKey found in message"); sourceListKey = c.SourceListKey; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs index 2fd02a25..5c04491b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs @@ -45,8 +45,6 @@ namespace PepperDash.Essentials.AppServer.Messengers private void SendFullStatus() { - Debug.Console(2, "LightingBaseMessenger GetFullStatus"); - var state = new LightingBaseStateMessage { Scenes = Device.LightingScenes, diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs index 8b3b5681..122dc883 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs @@ -1,5 +1,6 @@ using Newtonsoft.Json; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Room.Config; using System; @@ -51,7 +52,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } catch (Exception ex) { - Debug.Console(0, this, "Exception saving event: {0}\r\n{1}", ex.Message, ex.StackTrace); + this.LogException(ex,"Exception saving event"); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs index 8b7750a2..2cb2ced0 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/SystemMonitorMessenger.cs @@ -59,16 +59,14 @@ namespace PepperDash.Essentials.AppServer.Messengers foreach (var p in systemMonitor.ProgramStatusFeedbackCollection) { - PostStatusMessage(JToken.FromObject(p.Value.ProgramInfo) - ); + PostStatusMessage(JToken.FromObject(p.Value.ProgramInfo)); } } private void SendSystemMonitorStatusMessage() { - Debug.Console(1, "Posting System Monitor Status Message."); - // This takes a while, launch a new thread + Task.Run(() => PostStatusMessage(JToken.FromObject(new SystemMonitorStateMessage { diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs index fab0fe3b..a27e899b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/VideoCodecBaseMessenger.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; +using PepperDash.Core.Logging; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.DeviceTypeInterfaces; using PepperDash.Essentials.Devices.Common.Cameras; @@ -111,7 +112,7 @@ namespace PepperDash.Essentials.AppServer.Messengers if (Codec is IHasDirectory dirCodec) { - Debug.Console(2, this, "Sending Directory. Directory Item Count: {0}", directory.CurrentDirectoryResults.Count); + this.LogVerbose("Sending Directory. Directory Item Count: {directoryItemCount}", directory.CurrentDirectoryResults.Count); //state.CurrentDirectory = PrefixDirectoryFolderItems(directory); state.CurrentDirectory = directory; @@ -238,7 +239,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } if (Codec is IHasCodecCameras cameraCodec) { - Debug.Console(2, this, "Adding IHasCodecCameras Actions"); + this.LogVerbose("Adding IHasCodecCameras Actions"); cameraCodec.CameraSelected += CameraCodec_CameraSelected; @@ -254,7 +255,7 @@ namespace PepperDash.Essentials.AppServer.Messengers if (Codec is IHasCodecRoomPresets presetsCodec) { - Debug.Console(2, this, "Adding IHasCodecRoomPresets Actions"); + this.LogVerbose("Adding IHasCodecRoomPresets Actions"); presetsCodec.CodecRoomPresetsListHasChanged += PresetsCodec_CameraPresetsListHasChanged; @@ -275,7 +276,7 @@ namespace PepperDash.Essentials.AppServer.Messengers if (Codec is IHasCameraAutoMode speakerTrackCodec) { - Debug.Console(2, this, "Adding IHasCameraAutoMode Actions"); + this.LogVerbose("Adding IHasCameraAutoMode Actions"); speakerTrackCodec.CameraAutoModeIsOnFeedback.OutputChange += CameraAutoModeIsOnFeedback_OutputChange; @@ -286,7 +287,7 @@ namespace PepperDash.Essentials.AppServer.Messengers if (Codec is IHasCameraOff cameraOffCodec) { - Debug.Console(2, this, "Adding IHasCameraOff Actions"); + this.LogVerbose("Adding IHasCameraOff Actions"); cameraOffCodec.CameraIsOffFeedback.OutputChange += (CameraIsOffFeedback_OutputChange); @@ -298,7 +299,7 @@ namespace PepperDash.Essentials.AppServer.Messengers if (Codec is IHasCodecSelfView selfViewCodec) { - Debug.Console(2, this, "Adding IHasCodecSelfView Actions"); + this.LogVerbose("Adding IHasCodecSelfView Actions"); AddAction("/cameraSelfView", (id, content) => selfViewCodec.SelfViewModeToggle()); @@ -308,7 +309,7 @@ namespace PepperDash.Essentials.AppServer.Messengers if (Codec is IHasCodecLayouts layoutsCodec) { - Debug.Console(2, this, "Adding IHasCodecLayouts Actions"); + this.LogVerbose("Adding IHasCodecLayouts Actions"); AddAction("/cameraRemoteView", (id, content) => layoutsCodec.LocalLayoutToggle()); @@ -317,7 +318,7 @@ namespace PepperDash.Essentials.AppServer.Messengers if (Codec is IPasswordPrompt pwCodec) { - Debug.Console(2, this, "Adding IPasswordPrompt Actions"); + this.LogVerbose("Adding IPasswordPrompt Actions"); AddAction("/password", (id, content) => { @@ -334,7 +335,7 @@ namespace PepperDash.Essentials.AppServer.Messengers (sender, args) => PostReceivingContent(args.BoolValue); } - Debug.Console(2, this, "Adding Privacy & Standby Actions"); + this.LogVerbose("Adding Privacy & Standby Actions"); AddAction("/privacyModeOn", (id, content) => Codec.PrivacyModeOn()); AddAction("/privacyModeOff", (id, content) => Codec.PrivacyModeOff()); @@ -346,7 +347,7 @@ namespace PepperDash.Essentials.AppServer.Messengers } catch (Exception e) { - Debug.Console(2, this, "Error: {0}", e); + this.LogException(e, "Exception adding paths"); } } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index 111d4c2c..0990c65d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -26,16 +26,24 @@ pdbonly $(DefineConstants);SERIES4 + + + + + + + + - + false runtime - + false runtime diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs index 0c1f1087..9fc8cc41 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlDeviceFactory.cs @@ -31,54 +31,4 @@ namespace PepperDash.Essentials } } } - - public class MobileControlSimplFactory : EssentialsDeviceFactory - { - public MobileControlSimplFactory() - { - TypeNames = new List { "mobilecontrolbridge-ddvc01", "mobilecontrolbridge-simpl" }; - } - - public override EssentialsDevice BuildDevice(DeviceConfig dc) - { - var comm = CommFactory.GetControlPropertiesConfig(dc); - - var bridge = new MobileControlSIMPLRoomBridge(dc.Key, dc.Name, comm.IpIdInt); - - bridge.AddPreActivationAction(() => - { - var parent = GetMobileControlDevice(); - - if (parent == null) - { - bridge.LogInformation("ERROR: Cannot connect bridge. System controller not present"); - return; - } - bridge.LogInformation("Linking to parent controller"); - - parent.AddDeviceMessenger(bridge); - }); - - return bridge; - } - - private static MobileControlSystemController GetMobileControlDevice() - { - var mobileControlList = DeviceManager.AllDevices.OfType().ToList(); - - if (mobileControlList.Count > 1) - { - Debug.LogMessage(LogEventLevel.Warning, "Multiple instances of Mobile Control Server found."); - return null; - } - - if (mobileControlList.Count > 0) - { - return mobileControlList[0]; - } - - Debug.LogMessage(LogEventLevel.Warning, "Mobile Control not enabled for this system"); - return null; - } - } } \ No newline at end of file diff --git a/src/PepperDash.Essentials/ControlSystem.cs b/src/PepperDash.Essentials/ControlSystem.cs index 5955faaa..7a32a459 100644 --- a/src/PepperDash.Essentials/ControlSystem.cs +++ b/src/PepperDash.Essentials/ControlSystem.cs @@ -261,7 +261,7 @@ namespace PepperDash.Essentials _ = new DeviceFactory(); _ = new ProcessorExtensionDeviceFactory(); - _ = new MobileControl.MobileControlFactory(); + _ = new MobileControlFactory(); Debug.LogMessage(LogEventLevel.Information, "Starting Essentials load from configuration"); From 19d0bc73c8dd5d98ee4c2025a60135bb27bc85d1 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 22:50:31 -0500 Subject: [PATCH 18/26] fix: remove LightingBase from core --- .../Lighting/LightingBase.cs | 171 ------------------ .../Lighting/LightingScene.cs | 37 ++++ .../MobileControlSystemController.cs | 3 +- 3 files changed, 39 insertions(+), 172 deletions(-) delete mode 100644 src/PepperDash.Essentials.Core/Lighting/LightingBase.cs create mode 100644 src/PepperDash.Essentials.Core/Lighting/LightingScene.cs 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.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 52555867..2a6d0321 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -22,6 +22,7 @@ using PepperDash.Essentials.Core.Web; using PepperDash.Essentials.Devices.Common.AudioCodec; using PepperDash.Essentials.Devices.Common.Cameras; using PepperDash.Essentials.Devices.Common.Displays; +using PepperDash.Essentials.Devices.Common.Lighting; using PepperDash.Essentials.Devices.Common.SoftCodec; using PepperDash.Essentials.Devices.Common.VideoCodec; using PepperDash.Essentials.Room.MobileControl; @@ -492,7 +493,7 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is ILightingScenes) + if (device is ILightingScenes || device is LightingBase) { var deviceKey = device.Key; From 222c2f6fe26aaacbb0113b33d287daf5ed341896 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 22:52:32 -0500 Subject: [PATCH 19/26] chore: more miscellaneous cleanup --- .../Lighting/LightingBase.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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, From cdafaf758606694cf5263c2d2792df738453ea98 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 22:54:47 -0500 Subject: [PATCH 20/26] build(force-patch): remove cplz generation CPLZs are not required for using Essentials as a dependency, and just adds noise that's not required. --- .../PepperDash.Essentials.Core.csproj | 1 - .../PepperDash.Essentials.Devices.Common.csproj | 1 - .../PepperDash.Essentials.MobileControl.Messengers.csproj | 3 --- 3 files changed, 5 deletions(-) diff --git a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index 36c887ed..b31ce8c6 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -1,6 +1,5 @@  - ProgramLibrary Debug;Release;Debug 4.7.2 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 57d0b67c..966d778f 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 diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index 0990c65d..cc619d5b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -1,7 +1,4 @@  - - ProgramLibrary - PepperDash.Essentials.AppServer net472 From 26116d04955e44a11548495b0af25c31c221d7db Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 23:22:18 -0500 Subject: [PATCH 21/26] feat: move PD Core into Essentials --- PepperDash.Essentials.4Series.sln | 26 + src/Directory.Build.targets | 5 + .../Comm/CommunicationGather.cs | 179 +++ .../Comm/CommunicationStreamDebugging.cs | 177 +++ .../Comm/ControlPropertiesConfig.cs | 93 ++ src/PepperDash.Core/Comm/EventArgs.cs | 251 ++++ .../Comm/GenericSecureTcpIpClient.cs | 955 +++++++++++++++ .../GenericSecureTcpIpClient_ForServer.cs | 909 ++++++++++++++ .../Comm/GenericSecureTcpIpServer.cs | 1084 +++++++++++++++++ src/PepperDash.Core/Comm/GenericSshClient.cs | 592 +++++++++ .../Comm/GenericTcpIpClient.cs | 566 +++++++++ .../Comm/GenericTcpIpClient_ForServer.cs | 774 ++++++++++++ .../Comm/GenericTcpIpServer.cs | 1010 +++++++++++++++ src/PepperDash.Core/Comm/GenericUdpServer.cs | 395 ++++++ .../Comm/TcpClientConfigObject.cs | 59 + .../Comm/TcpServerConfigObject.cs | 60 + src/PepperDash.Core/Comm/eControlMethods.cs | 79 ++ src/PepperDash.Core/CommunicationExtras.cs | 247 ++++ .../Config/PortalConfigReader.cs | 235 ++++ src/PepperDash.Core/Conversion/Convert.cs | 22 + src/PepperDash.Core/CoreInterfaces.cs | 35 + src/PepperDash.Core/Device.cs | 179 +++ src/PepperDash.Core/Directory.build.targets | 43 + ...sentialsPlugins-builds-4-series-caller.yml | 21 + src/PepperDash.Core/EthernetHelper.cs | 116 ++ src/PepperDash.Core/EventArgs.cs | 172 +++ .../GenericRESTfulCommunications/Constants.cs | 39 + .../GenericRESTfulClient.cs | 256 ++++ .../EventArgs and Constants.cs | 77 ++ .../JsonStandardObjects/JsonToSimplDevice.cs | 183 +++ .../JsonToSimplDeviceConfig.cs | 257 ++++ src/PepperDash.Core/JsonToSimpl/Constants.cs | 143 +++ src/PepperDash.Core/JsonToSimpl/Global.cs | 59 + .../JsonToSimplArrayLookupChild.cs | 162 +++ .../JsonToSimpl/JsonToSimplChildObjectBase.cs | 404 ++++++ .../JsonToSimpl/JsonToSimplFileMaster.cs | 289 +++++ .../JsonToSimpl/JsonToSimplFixedPathObject.cs | 18 + .../JsonToSimpl/JsonToSimplGenericMaster.cs | 118 ++ .../JsonToSimpl/JsonToSimplMaster.cs | 247 ++++ .../JsonToSimplPortalFileMaster.cs | 191 +++ .../Logging/CrestronEnricher.cs | 37 + src/PepperDash.Core/Logging/Debug.cs | 895 ++++++++++++++ .../Logging/DebugConsoleSink.cs | 55 + src/PepperDash.Core/Logging/DebugContext.cs | 281 +++++ .../Logging/DebugCrestronLoggerSink.cs | 29 + .../Logging/DebugErrorLogSink.cs | 65 + .../Logging/DebugExtensions.cs | 44 + src/PepperDash.Core/Logging/DebugMemory.cs | 115 ++ .../Logging/DebugWebsocketSink.cs | 271 +++++ .../Network/DiscoveryThings.cs | 22 + .../PasswordManagement/Config.cs | 26 + .../PasswordManagement/Constants.cs | 57 + .../PasswordManagement/PasswordClient.cs | 187 +++ .../PasswordManagement/PasswordManager.cs | 241 ++++ src/PepperDash.Core/PepperDash.Core.csproj | 66 + .../net472/PepperDashCore.props | 18 + .../net472/PepperDashCore.targets | 57 + .../Properties/ControlSystem.cfg | 7 + .../SystemInfo/EventArgs and Constants.cs | 264 ++++ .../SystemInfo/SystemInfoConfig.cs | 204 ++++ .../SystemInfo/SystemInfoToSimpl.cs | 462 +++++++ src/PepperDash.Core/Web/BouncyCertificate.cs | 356 ++++++ .../RequestHandlers/DefaultRequestHandler.cs | 17 + .../WebApiBaseRequestAsyncHandler.cs | 163 +++ .../WebApiBaseRequestHandler.cs | 165 +++ src/PepperDash.Core/Web/WebApiServer.cs | 284 +++++ src/PepperDash.Core/WebApi/Presets/Preset.cs | 87 ++ src/PepperDash.Core/WebApi/Presets/User.cs | 93 ++ .../WebApi/Presets/WebApiPasscodeClient.cs | 273 +++++ .../Serialization/IXSigSerialization.cs | 25 + .../XSigSerializationException.cs | 28 + .../XSigUtility/Tokens/XSigAnalogToken.cs | 88 ++ .../XSigUtility/Tokens/XSigDigitalToken.cs | 85 ++ .../XSigUtility/Tokens/XSigSerialToken.cs | 81 ++ .../XSigUtility/Tokens/XSigToken.cs | 45 + .../XSigUtility/Tokens/XSigTokenType.cs | 23 + .../XSigUtility/XSigHelpers.cs | 239 ++++ .../XSigUtility/XSigTokenStreamReader.cs | 147 +++ .../XSigUtility/XSigTokenStreamWriter.cs | 136 +++ .../PepperDash.Essentials.Core.csproj | 4 +- ...epperDash.Essentials.Devices.Common.csproj | 2 +- ...Essentials.MobileControl.Messengers.csproj | 2 +- ...PepperDash.Essentials.MobileControl.csproj | 2 +- .../PepperDash.Essentials.csproj | 2 +- 84 files changed, 16472 insertions(+), 5 deletions(-) create mode 100644 src/PepperDash.Core/Comm/CommunicationGather.cs create mode 100644 src/PepperDash.Core/Comm/CommunicationStreamDebugging.cs create mode 100644 src/PepperDash.Core/Comm/ControlPropertiesConfig.cs create mode 100644 src/PepperDash.Core/Comm/EventArgs.cs create mode 100644 src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs create mode 100644 src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs create mode 100644 src/PepperDash.Core/Comm/GenericSecureTcpIpServer.cs create mode 100644 src/PepperDash.Core/Comm/GenericSshClient.cs create mode 100644 src/PepperDash.Core/Comm/GenericTcpIpClient.cs create mode 100644 src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs create mode 100644 src/PepperDash.Core/Comm/GenericTcpIpServer.cs create mode 100644 src/PepperDash.Core/Comm/GenericUdpServer.cs create mode 100644 src/PepperDash.Core/Comm/TcpClientConfigObject.cs create mode 100644 src/PepperDash.Core/Comm/TcpServerConfigObject.cs create mode 100644 src/PepperDash.Core/Comm/eControlMethods.cs create mode 100644 src/PepperDash.Core/CommunicationExtras.cs create mode 100644 src/PepperDash.Core/Config/PortalConfigReader.cs create mode 100644 src/PepperDash.Core/Conversion/Convert.cs create mode 100644 src/PepperDash.Core/CoreInterfaces.cs create mode 100644 src/PepperDash.Core/Device.cs create mode 100644 src/PepperDash.Core/Directory.build.targets create mode 100644 src/PepperDash.Core/EssentialsPlugins-builds-4-series-caller.yml create mode 100644 src/PepperDash.Core/EthernetHelper.cs create mode 100644 src/PepperDash.Core/EventArgs.cs create mode 100644 src/PepperDash.Core/GenericRESTfulCommunications/Constants.cs create mode 100644 src/PepperDash.Core/GenericRESTfulCommunications/GenericRESTfulClient.cs create mode 100644 src/PepperDash.Core/JsonStandardObjects/EventArgs and Constants.cs create mode 100644 src/PepperDash.Core/JsonStandardObjects/JsonToSimplDevice.cs create mode 100644 src/PepperDash.Core/JsonStandardObjects/JsonToSimplDeviceConfig.cs create mode 100644 src/PepperDash.Core/JsonToSimpl/Constants.cs create mode 100644 src/PepperDash.Core/JsonToSimpl/Global.cs create mode 100644 src/PepperDash.Core/JsonToSimpl/JsonToSimplArrayLookupChild.cs create mode 100644 src/PepperDash.Core/JsonToSimpl/JsonToSimplChildObjectBase.cs create mode 100644 src/PepperDash.Core/JsonToSimpl/JsonToSimplFileMaster.cs create mode 100644 src/PepperDash.Core/JsonToSimpl/JsonToSimplFixedPathObject.cs create mode 100644 src/PepperDash.Core/JsonToSimpl/JsonToSimplGenericMaster.cs create mode 100644 src/PepperDash.Core/JsonToSimpl/JsonToSimplMaster.cs create mode 100644 src/PepperDash.Core/JsonToSimpl/JsonToSimplPortalFileMaster.cs create mode 100644 src/PepperDash.Core/Logging/CrestronEnricher.cs create mode 100644 src/PepperDash.Core/Logging/Debug.cs create mode 100644 src/PepperDash.Core/Logging/DebugConsoleSink.cs create mode 100644 src/PepperDash.Core/Logging/DebugContext.cs create mode 100644 src/PepperDash.Core/Logging/DebugCrestronLoggerSink.cs create mode 100644 src/PepperDash.Core/Logging/DebugErrorLogSink.cs create mode 100644 src/PepperDash.Core/Logging/DebugExtensions.cs create mode 100644 src/PepperDash.Core/Logging/DebugMemory.cs create mode 100644 src/PepperDash.Core/Logging/DebugWebsocketSink.cs create mode 100644 src/PepperDash.Core/Network/DiscoveryThings.cs create mode 100644 src/PepperDash.Core/PasswordManagement/Config.cs create mode 100644 src/PepperDash.Core/PasswordManagement/Constants.cs create mode 100644 src/PepperDash.Core/PasswordManagement/PasswordClient.cs create mode 100644 src/PepperDash.Core/PasswordManagement/PasswordManager.cs create mode 100644 src/PepperDash.Core/PepperDash.Core.csproj create mode 100644 src/PepperDash.Core/PepperDashCore.build/net472/PepperDashCore.props create mode 100644 src/PepperDash.Core/PepperDashCore.build/net472/PepperDashCore.targets create mode 100644 src/PepperDash.Core/Properties/ControlSystem.cfg create mode 100644 src/PepperDash.Core/SystemInfo/EventArgs and Constants.cs create mode 100644 src/PepperDash.Core/SystemInfo/SystemInfoConfig.cs create mode 100644 src/PepperDash.Core/SystemInfo/SystemInfoToSimpl.cs create mode 100644 src/PepperDash.Core/Web/BouncyCertificate.cs create mode 100644 src/PepperDash.Core/Web/RequestHandlers/DefaultRequestHandler.cs create mode 100644 src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestAsyncHandler.cs create mode 100644 src/PepperDash.Core/Web/RequestHandlers/WebApiBaseRequestHandler.cs create mode 100644 src/PepperDash.Core/Web/WebApiServer.cs create mode 100644 src/PepperDash.Core/WebApi/Presets/Preset.cs create mode 100644 src/PepperDash.Core/WebApi/Presets/User.cs create mode 100644 src/PepperDash.Core/WebApi/Presets/WebApiPasscodeClient.cs create mode 100644 src/PepperDash.Core/XSigUtility/Serialization/IXSigSerialization.cs create mode 100644 src/PepperDash.Core/XSigUtility/Serialization/XSigSerializationException.cs create mode 100644 src/PepperDash.Core/XSigUtility/Tokens/XSigAnalogToken.cs create mode 100644 src/PepperDash.Core/XSigUtility/Tokens/XSigDigitalToken.cs create mode 100644 src/PepperDash.Core/XSigUtility/Tokens/XSigSerialToken.cs create mode 100644 src/PepperDash.Core/XSigUtility/Tokens/XSigToken.cs create mode 100644 src/PepperDash.Core/XSigUtility/Tokens/XSigTokenType.cs create mode 100644 src/PepperDash.Core/XSigUtility/XSigHelpers.cs create mode 100644 src/PepperDash.Core/XSigUtility/XSigTokenStreamReader.cs create mode 100644 src/PepperDash.Core/XSigUtility/XSigTokenStreamWriter.cs diff --git a/PepperDash.Essentials.4Series.sln b/PepperDash.Essentials.4Series.sln index a1e57f5e..7423c50a 100644 --- a/PepperDash.Essentials.4Series.sln +++ b/PepperDash.Essentials.4Series.sln @@ -4,19 +4,38 @@ Microsoft Visual Studio Solution File, Format Version 12.00 VisualStudioVersion = 17.4.33213.308 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Devices.Common", "src\PepperDash.Essentials.Devices.Common\PepperDash.Essentials.Devices.Common.csproj", "{53E204B7-97DD-441D-A96C-721DF014DF82}" + ProjectSection(ProjectDependencies) = postProject + {E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials", "src\PepperDash.Essentials\PepperDash.Essentials.csproj", "{CB3B11BA-625C-4D35-B663-FDC5BE9A230E}" + ProjectSection(ProjectDependencies) = postProject + {E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PepperDash.Essentials.Core", "src\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj", "{3D192FED-8FFC-4CB5-B5F7-BA307ABA254B}" + ProjectSection(ProjectDependencies) = postProject + {E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mobile Control", "Mobile Control", "{B24989D7-32B5-48D5-9AE1-5F3B17D25206}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl", "src\PepperDash.Essentials.MobileControl\PepperDash.Essentials.MobileControl.csproj", "{F6D362DE-2256-44B1-927A-8CE4705D839A}" + ProjectSection(ProjectDependencies) = postProject + {E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90} + EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Essentials.MobileControl.Messengers", "src\PepperDash.Essentials.MobileControl.Messengers\PepperDash.Essentials.MobileControl.Messengers.csproj", "{B438694F-8FF7-464A-9EC8-10427374471F}" + ProjectSection(ProjectDependencies) = postProject + {E5336563-1194-501E-BC4A-79AD9283EF90} = {E5336563-1194-501E-BC4A-79AD9283EF90} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Essentials", "Essentials", "{AD98B742-8D85-481C-A69D-D8D8ABED39EA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PepperDash.Core", "src\PepperDash.Core\PepperDash.Core.csproj", "{E5336563-1194-501E-BC4A-79AD9283EF90}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug 4.7.2|Any CPU = Debug 4.7.2|Any CPU @@ -54,6 +73,12 @@ Global {B438694F-8FF7-464A-9EC8-10427374471F}.Debug|Any CPU.Build.0 = Debug|Any CPU {B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.ActiveCfg = Release|Any CPU {B438694F-8FF7-464A-9EC8-10427374471F}.Release|Any CPU.Build.0 = Release|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.ActiveCfg = Debug|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug 4.7.2|Any CPU.Build.0 = Debug|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5336563-1194-501E-BC4A-79AD9283EF90}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -64,6 +89,7 @@ Global {3D192FED-8FFC-4CB5-B5F7-BA307ABA254B} = {AD98B742-8D85-481C-A69D-D8D8ABED39EA} {F6D362DE-2256-44B1-927A-8CE4705D839A} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206} {B438694F-8FF7-464A-9EC8-10427374471F} = {B24989D7-32B5-48D5-9AE1-5F3B17D25206} + {E5336563-1194-501E-BC4A-79AD9283EF90} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6907A4BF-7201-47CF-AAB1-3597F3B8E1C3} 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..7531f9e9 --- /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) + { + 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) 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..447632e3 --- /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(ex, "DequeueEvent error"); + } + // Make sure to leave the CCritical section in case an exception above stops this thread, or we won't be able to restart it. + if (DequeueLock != null) + { + DequeueLock.Leave(); + } + } + + void HeartbeatStart() + { + if (HeartbeatEnabled) + { + Debug.Console(2, this, "Starting Heartbeat"); + if (HeartbeatSendTimer == null) + { + + HeartbeatSendTimer = new CTimer(this.SendHeartbeat, null, HeartbeatInterval, HeartbeatInterval); + } + if (HeartbeatAckTimer == null) + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + } + + } + void HeartbeatStop() + { + + if (HeartbeatSendTimer != null) + { + Debug.Console(2, this, "Stoping Heartbeat Send"); + HeartbeatSendTimer.Stop(); + HeartbeatSendTimer = null; + } + if (HeartbeatAckTimer != null) + { + Debug.Console(2, this, "Stoping Heartbeat Ack"); + HeartbeatAckTimer.Stop(); + HeartbeatAckTimer = null; + } + + } + void SendHeartbeat(object notused) + { + this.SendText(HeartbeatString); + Debug.Console(2, this, "Sending Heartbeat"); + + } + + //private method to check heartbeat requirements and start or reset timer + string checkHeartbeat(string received) + { + try + { + if (HeartbeatEnabled) + { + if (!string.IsNullOrEmpty(HeartbeatString)) + { + var remainingText = received.Replace(HeartbeatString, ""); + var noDelimiter = received.Trim(new char[] { '\r', '\n' }); + if (noDelimiter.Contains(HeartbeatString)) + { + if (HeartbeatAckTimer != null) + { + HeartbeatAckTimer.Reset(HeartbeatInterval * 2); + } + else + { + HeartbeatAckTimer = new CTimer(HeartbeatAckTimerFail, null, (HeartbeatInterval * 2), (HeartbeatInterval * 2)); + } + Debug.Console(2, this, "Heartbeat Received: {0}, from Server", HeartbeatString); + return remainingText; + } + } + } + } + catch (Exception ex) + { + Debug.Console(1, this, "Error checking heartbeat: {0}", ex.Message); + } + return received; + } + + + + void HeartbeatAckTimerFail(object o) + { + try + { + + if (IsConnected) + { + Debug.Console(1, Debug.ErrorLogLevel.Warning, "Heartbeat not received from Server...DISCONNECTING BECAUSE HEARTBEAT REQUIRED IS TRUE"); + SendText("Heartbeat not received by server, closing connection"); + CheckClosedAndTryReconnect(); + } + + } + catch (Exception ex) + { + ErrorLog.Error("Heartbeat timeout Error on Client: {0}, {1}", Key, ex); + } + } + + /// + /// + /// + 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..df277f00 --- /dev/null +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs @@ -0,0 +1,774 @@ +/*PepperDash Technology Corp. +JAG +Copyright: 2017 +------------------------------------ +***Notice of Ownership and Copyright*** +The material in which this notice appears is the property of PepperDash Technology Corporation, +which claims copyright under the laws of the United States of America in the entire body of material +and in all parts thereof, regardless of the use to which it is being put. Any use, in whole or in part, +of this material by another party without the express written permission of PepperDash Technology Corporation is prohibited. +PepperDash Technology Corporation reserves all rights under applicable laws. +------------------------------------ */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronSockets; + +namespace PepperDash.Core +{ + /// + /// 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() + { + Debug.Console(2, "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..680db5c5 --- /dev/null +++ b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs @@ -0,0 +1,1010 @@ +/*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; + +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) + Debug.Console(2, 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..e31c581c --- /dev/null +++ b/src/PepperDash.Core/Comm/GenericUdpServer.cs @@ -0,0 +1,395 @@ + +using System; +using System.Linq; +using System.Text; + +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronSockets; +using Newtonsoft.Json; + +namespace PepperDash.Core +{ + /// + /// 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) + { + Debug.Console(0, "GenericUdpServer Receive error: {0}{1}", ex.Message, ex.StackTrace); + } + 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..0ccc50aa --- /dev/null +++ b/src/PepperDash.Core/EthernetHelper.cs @@ -0,0 +1,116 @@ +using Crestron.SimplSharp; +using Newtonsoft.Json; + +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.Console(0, "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..be2e7951 --- /dev/null +++ b/src/PepperDash.Core/JsonToSimpl/Global.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Crestron.SimplSharp; + +//using PepperDash.Core; + +namespace PepperDash.Core.JsonToSimpl +{ + /// + /// 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.Console(1, "JSON Global adding master {0}", master.UniqueID); + + if (Masters.Contains(master)) return; + + var existing = Masters.FirstOrDefault(m => + m.UniqueID.Equals(master.UniqueID, StringComparison.OrdinalIgnoreCase)); + if (existing == null) + { + Masters.Add(master); + } + else + { + var msg = string.Format("Cannot add JSON Master with unique ID '{0}'.\rID is already in use on another master.", master.UniqueID); + CrestronConsole.PrintLine(msg); + ErrorLog.Warn(msg); + } + } + + /// + /// 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..4e79a601 --- /dev/null +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -0,0 +1,895 @@ +using Crestron.SimplSharp; +using Crestron.SimplSharp.CrestronDataStore; +using Crestron.SimplSharp.CrestronIO; +using Crestron.SimplSharp.CrestronLogger; +using Newtonsoft.Json; +using PepperDash.Core.Logging; +using Serilog; +using Serilog.Context; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting.Compact; +using Serilog.Formatting.Json; +using Serilog.Templates; +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace PepperDash.Core +{ + /// + /// 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) => + { + Console(0, "Console debug level set to {0}", _consoleLoggingLevelSwitch.MinimumLevel); + }; + } + + public static void UpdateLoggerConfiguration(LoggerConfiguration config) + { + _loggerConfiguration = config; + + _logger = config.CreateLogger(); + } + + public static void ResetLoggerConfiguration() + { + _loggerConfiguration = _defaultLoggerConfiguration; + + _logger = _loggerConfiguration.CreateLogger(); + } + + private static LogEventLevel GetStoredLogEventLevel(string levelStoreKey) + { + try + { + var result = CrestronDataStoreStatic.GetLocalIntValue(levelStoreKey, out int logLevel); + + if (result != CrestronDataStore.CDS_ERROR.CDS_SUCCESS) + { + CrestronConsole.Print($"Unable to retrieve stored log level for {levelStoreKey}.\r\nError: {result}.\r\nSetting level to {LogEventLevel.Information}\r\n"); + return LogEventLevel.Information; + } + + if(logLevel < 0 || logLevel > 5) + { + CrestronConsole.PrintLine($"Stored Log level not valid for {levelStoreKey}: {logLevel}. Setting level to {LogEventLevel.Information}"); + return LogEventLevel.Information; + } + + return (LogEventLevel)logLevel; + } catch (Exception ex) + { + CrestronConsole.PrintLine($"Exception retrieving log level for {levelStoreKey}: {ex.Message}"); + return LogEventLevel.Information; + } + } + + private static void GetVersion() + { + var assembly = Assembly.GetExecutingAssembly(); + var ver = + assembly + .GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), false); + + if (ver != null && ver.Length > 0) + { + if (ver[0] is AssemblyInformationalVersionAttribute verAttribute) + { + PepperDashCoreVersion = verAttribute.InformationalVersion; + } + } + else + { + var version = assembly.GetName().Version; + PepperDashCoreVersion = version.ToString(); + } + } + + /// + /// 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; + } + Console(0, "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 using Serilog's default Exception logging mechanism + /// + /// 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) + { + LogMessage(level, message, null, args); + } + + public static void LogMessage(LogEventLevel level, IKeyed keyed, string message, params object[] args) + { + LogMessage(level, message, keyed, args); + } + + + private static void LogMessage(uint level, string format, params object[] items) + { + if (!_logLevels.ContainsKey(level)) return; + + var logLevel = _logLevels[level]; + + LogMessage(logLevel, format, items); + } + + private static void LogMessage(uint level, IKeyed keyed, string format, params object[] items) + { + if (!_logLevels.ContainsKey(level)) return; + + var logLevel = _logLevels[level]; + + LogMessage(logLevel, keyed, format, items); + } + + + /// + /// 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")] + 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")] + 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")] + 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")] + 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")] + 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")] + 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")] + 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(); + + Console(0, ErrorLogLevel.Notice, "Loading debug settings file from {0}", fileName); + + using (var sw = new StreamWriter(fileName)) + { + var json = JsonConvert.SerializeObject(_contexts); + sw.Write(json); + sw.Flush(); + } + } + + /// + /// + /// + 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) + { + Console(1, "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); + } + + private static void CheckForMigration() + { + var oldFilePath = String.Format(@"\nvram\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); + var newFilePath = String.Format(@"\user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); + + //check for file at old path + if (!File.Exists(oldFilePath)) + { + Console(0, ErrorLogLevel.Notice, + String.Format( + @"Debug settings file migration not necessary. Using file at \user\debugSettings\program{0}", + InitialParametersClass.ApplicationNumber)); + + return; + } + + //create the new directory if it doesn't already exist + if (!Directory.Exists(@"\user\debugSettings")) + { + Directory.CreateDirectory(@"\user\debugSettings"); + } + + Console(0, ErrorLogLevel.Notice, + String.Format( + @"File found at \nvram\debugSettings\program{0}. Migrating to \user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber)); + + //Copy file from old path to new path, then delete it. This will overwrite the existing file + File.Copy(oldFilePath, newFilePath, true); + File.Delete(oldFilePath); + + //Check if the old directory is empty, then delete it if it is + if (Directory.GetFiles(@"\nvram\debugSettings").Length > 0) + { + return; + } + + Directory.Delete(@"\nvram\debugSettings"); + } + + /// + /// 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..68e9868b --- /dev/null +++ b/src/PepperDash.Core/Logging/DebugExtensions.cs @@ -0,0 +1,44 @@ +using Serilog; +using Serilog.Events; +using System; +using Log = PepperDash.Core.Debug; + +namespace PepperDash.Core.Logging +{ + public static class DebugExtensions + { + public static void LogException(this IKeyed device, Exception ex, string message, params object[] args) + { + Log.LogMessage(ex, message, device, args); + } + public static void LogVerbose(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Verbose, device, message, args); + } + + public static void LogDebug(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Debug, device, message, args); + } + + public static void LogInformation(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Information, device, message, args); + } + + public static void LogWarning(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Warning, device, message, args); + } + + public static void LogError(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Error, device, message, args); + } + + public static void LogFatal(this IKeyed device, string message, params object[] args) + { + Log.LogMessage(LogEventLevel.Fatal, device, message, args); + } + } +} 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/PepperDash.Essentials.Core.csproj b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj index b31ce8c6..b1406d69 100644 --- a/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj +++ b/src/PepperDash.Essentials.Core/PepperDash.Essentials.Core.csproj @@ -25,7 +25,6 @@
- @@ -33,4 +32,7 @@ + + +
\ No newline at end of file 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 966d778f..49c05762 100644 --- a/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj +++ b/src/PepperDash.Essentials.Devices.Common/PepperDash.Essentials.Devices.Common.csproj @@ -24,10 +24,10 @@ pdbonly
+ -
\ No newline at end of file diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj index cc619d5b..8739c95b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj +++ b/src/PepperDash.Essentials.MobileControl.Messengers/PepperDash.Essentials.MobileControl.Messengers.csproj @@ -33,9 +33,9 @@
- + false runtime diff --git a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj index c32f226f..1e887728 100644 --- a/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj +++ b/src/PepperDash.Essentials.MobileControl/PepperDash.Essentials.MobileControl.csproj @@ -38,7 +38,6 @@ - @@ -53,6 +52,7 @@ + false runtime diff --git a/src/PepperDash.Essentials/PepperDash.Essentials.csproj b/src/PepperDash.Essentials/PepperDash.Essentials.csproj index 660daaa4..04ae0d86 100644 --- a/src/PepperDash.Essentials/PepperDash.Essentials.csproj +++ b/src/PepperDash.Essentials/PepperDash.Essentials.csproj @@ -48,9 +48,9 @@ - + From a5bc79c46902eeb768bb1211b233e97114b862dc Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Tue, 25 Mar 2025 23:58:51 -0500 Subject: [PATCH 22/26] chore: update some logging methods --- src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs | 2 +- src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs | 2 +- src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs | 3 ++- src/PepperDash.Core/Comm/GenericTcpIpServer.cs | 3 ++- src/PepperDash.Core/Comm/GenericUdpServer.cs | 3 ++- src/PepperDash.Core/EthernetHelper.cs | 3 ++- src/PepperDash.Core/JsonToSimpl/Global.cs | 3 ++- 7 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs index 7531f9e9..5ad2e29d 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient.cs @@ -741,7 +741,7 @@ namespace PepperDash.Core { if (HeartbeatEnabled) { - Debug.Console(2, this, "Starting Heartbeat"); + this.LogVerbose("Starting Heartbeat"); if (HeartbeatSendTimer == null) { diff --git a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs index 447632e3..93b195ca 100644 --- a/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs +++ b/src/PepperDash.Core/Comm/GenericSecureTcpIpClient_ForServer.cs @@ -686,7 +686,7 @@ namespace PepperDash.Core } catch (Exception e) { - this.LogException(ex, "DequeueEvent error"); + 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) diff --git a/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs b/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs index df277f00..03a27827 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpClient_ForServer.cs @@ -17,6 +17,7 @@ using System.Text; using System.Text.RegularExpressions; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; +using PepperDash.Core.Logging; namespace PepperDash.Core { @@ -445,7 +446,7 @@ namespace PepperDash.Core ///
public void Disconnect() { - Debug.Console(2, "Disconnect Called"); + this.LogVerbose("Disconnect Called"); DisconnectCalledByUser = true; if (IsConnected) diff --git a/src/PepperDash.Core/Comm/GenericTcpIpServer.cs b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs index 680db5c5..6aa5e6b5 100644 --- a/src/PepperDash.Core/Comm/GenericTcpIpServer.cs +++ b/src/PepperDash.Core/Comm/GenericTcpIpServer.cs @@ -15,6 +15,7 @@ using System.Linq; using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; +using PepperDash.Core.Logging; namespace PepperDash.Core { @@ -526,7 +527,7 @@ namespace PepperDash.Core { SocketErrorCodes error = myTcpServer.SendDataAsync(i, b, b.Length, (x, y, z) => { }); if (error != SocketErrorCodes.SOCKET_OK && error != SocketErrorCodes.SOCKET_OPERATION_PENDING) - Debug.Console(2, error.ToString()); + this.LogError("{error}",error.ToString()); } } } diff --git a/src/PepperDash.Core/Comm/GenericUdpServer.cs b/src/PepperDash.Core/Comm/GenericUdpServer.cs index e31c581c..a5a68c45 100644 --- a/src/PepperDash.Core/Comm/GenericUdpServer.cs +++ b/src/PepperDash.Core/Comm/GenericUdpServer.cs @@ -6,6 +6,7 @@ using System.Text; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronSockets; using Newtonsoft.Json; +using PepperDash.Core.Logging; namespace PepperDash.Core { @@ -279,7 +280,7 @@ namespace PepperDash.Core } catch (Exception ex) { - Debug.Console(0, "GenericUdpServer Receive error: {0}{1}", ex.Message, ex.StackTrace); + this.LogException(ex, "GenericUdpServer Receive error"); } finally { diff --git a/src/PepperDash.Core/EthernetHelper.cs b/src/PepperDash.Core/EthernetHelper.cs index 0ccc50aa..88429886 100644 --- a/src/PepperDash.Core/EthernetHelper.cs +++ b/src/PepperDash.Core/EthernetHelper.cs @@ -1,5 +1,6 @@ using Crestron.SimplSharp; using Newtonsoft.Json; +using Serilog.Events; namespace PepperDash.Core { @@ -43,7 +44,7 @@ namespace PepperDash.Core { var status = CrestronEthernetHelper.GetEthernetParameter( CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_LINK_STATUS, 0); - Debug.Console(0, "LinkActive = {0}", status); + Debug.LogMessage(LogEventLevel.Information, "LinkActive = {0}", status); return status == ""; } } diff --git a/src/PepperDash.Core/JsonToSimpl/Global.cs b/src/PepperDash.Core/JsonToSimpl/Global.cs index be2e7951..8392fa61 100644 --- a/src/PepperDash.Core/JsonToSimpl/Global.cs +++ b/src/PepperDash.Core/JsonToSimpl/Global.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Crestron.SimplSharp; +using Serilog.Events; //using PepperDash.Core; @@ -30,7 +31,7 @@ namespace PepperDash.Core.JsonToSimpl if (string.IsNullOrEmpty(master.UniqueID)) throw new InvalidOperationException("JSON Master cannot be added with a null UniqueId"); - Debug.Console(1, "JSON Global adding master {0}", master.UniqueID); + Debug.LogMessage(LogEventLevel.Debug, "JSON Global adding master {0}", master.UniqueID); if (Masters.Contains(master)) return; From 7629836732ef812582a4b7535d88dc357428d985 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 26 Mar 2025 00:01:06 -0500 Subject: [PATCH 23/26] feat: add overloads for specific levels In an effort to make it easier to use the logging mechanism, I added 4 overloaded methods for each level to allow for logging exceptions at any level, not just error. I also added overloads for each level to the extensions so that an exception can be logged at any level, not just error. --- src/PepperDash.Core/Logging/Debug.cs | 239 ++++++++++++++---- .../Logging/DebugExtensions.cs | 34 ++- 2 files changed, 218 insertions(+), 55 deletions(-) diff --git a/src/PepperDash.Core/Logging/Debug.cs b/src/PepperDash.Core/Logging/Debug.cs index 4e79a601..38dfa034 100644 --- a/src/PepperDash.Core/Logging/Debug.cs +++ b/src/PepperDash.Core/Logging/Debug.cs @@ -229,7 +229,7 @@ namespace PepperDash.Core _consoleLoggingLevelSwitch.MinimumLevelChanged += (sender, args) => { - Console(0, "Console debug level set to {0}", _consoleLoggingLevelSwitch.MinimumLevel); + LogMessage(LogEventLevel.Information, "Console debug level set to {minimumLevel}", _consoleLoggingLevelSwitch.MinimumLevel); }; } @@ -310,7 +310,7 @@ namespace PepperDash.Core _saveTimer.Stop(); _saveTimer = null; } - Console(0, "Saving debug settings"); + LogMessage(LogEventLevel.Information, "Saving debug settings"); SaveMemory(); } } @@ -589,7 +589,7 @@ namespace PepperDash.Core } /// - /// Log an Exception using Serilog's default Exception logging mechanism + /// Log an Exception as an Error /// /// Exception to log /// Message template @@ -610,7 +610,7 @@ namespace PepperDash.Core /// 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) + public static void LogMessage(LogEventLevel level, string message, IKeyed device = null, params object[] args) { using (LogContext.PushProperty("Key", device?.Key)) { @@ -620,7 +620,12 @@ namespace PepperDash.Core public static void LogMessage(LogEventLevel level, string message, params object[] args) { - LogMessage(level, message, null, 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) @@ -628,6 +633,173 @@ namespace PepperDash.Core 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) { @@ -655,7 +827,7 @@ namespace PepperDash.Core /// /// Console format string /// Object parameters - [Obsolete("Use LogMessage methods")] + [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) { @@ -673,7 +845,7 @@ namespace PepperDash.Core /// /// Logs to Console when at-level, and all messages to error log, including device key /// - [Obsolete("Use LogMessage methods")] + [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); @@ -686,7 +858,7 @@ namespace PepperDash.Core /// 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")] + [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) { @@ -696,7 +868,7 @@ namespace PepperDash.Core /// /// Logs to Console when at-level, and all messages to error log /// - [Obsolete("Use LogMessage methods")] + [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) { @@ -708,7 +880,7 @@ namespace PepperDash.Core /// 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")] + [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); @@ -724,7 +896,7 @@ namespace PepperDash.Core /// 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")] + [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); @@ -738,7 +910,7 @@ namespace PepperDash.Core ///
/// /// - [Obsolete("Use LogMessage methods")] + [Obsolete("Use LogMessage methods, Will be removed in 2.2.0 and later versions")] public static void LogError(ErrorLogLevel errorLogLevel, string str) { switch (errorLogLevel) @@ -782,7 +954,7 @@ namespace PepperDash.Core var fileName = GetMemoryFileName(); - Console(0, ErrorLogLevel.Notice, "Loading debug settings file from {0}", fileName); + LogMessage(LogEventLevel.Information, "Loading debug settings file from {fileName}", fileName); using (var sw = new StreamWriter(fileName)) { @@ -807,7 +979,7 @@ namespace PepperDash.Core if (_contexts != null) { - Console(1, "Debug memory restored from file"); + LogMessage(LogEventLevel.Debug, "Debug memory restored from file"); return; } } @@ -830,45 +1002,6 @@ namespace PepperDash.Core return string.Format("{0}{1}user{1}debugSettings{1}{2}.json",Directory.GetApplicationRootDirectory(), Path.DirectorySeparatorChar, InitialParametersClass.RoomId); } - private static void CheckForMigration() - { - var oldFilePath = String.Format(@"\nvram\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); - var newFilePath = String.Format(@"\user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber); - - //check for file at old path - if (!File.Exists(oldFilePath)) - { - Console(0, ErrorLogLevel.Notice, - String.Format( - @"Debug settings file migration not necessary. Using file at \user\debugSettings\program{0}", - InitialParametersClass.ApplicationNumber)); - - return; - } - - //create the new directory if it doesn't already exist - if (!Directory.Exists(@"\user\debugSettings")) - { - Directory.CreateDirectory(@"\user\debugSettings"); - } - - Console(0, ErrorLogLevel.Notice, - String.Format( - @"File found at \nvram\debugSettings\program{0}. Migrating to \user\debugSettings\program{0}", InitialParametersClass.ApplicationNumber)); - - //Copy file from old path to new path, then delete it. This will overwrite the existing file - File.Copy(oldFilePath, newFilePath, true); - File.Delete(oldFilePath); - - //Check if the old directory is empty, then delete it if it is - if (Directory.GetFiles(@"\nvram\debugSettings").Length > 0) - { - return; - } - - Directory.Delete(@"\nvram\debugSettings"); - } - /// /// Error level to for message to be logged at /// diff --git a/src/PepperDash.Core/Logging/DebugExtensions.cs b/src/PepperDash.Core/Logging/DebugExtensions.cs index 68e9868b..a8b7bd55 100644 --- a/src/PepperDash.Core/Logging/DebugExtensions.cs +++ b/src/PepperDash.Core/Logging/DebugExtensions.cs @@ -1,5 +1,4 @@ -using Serilog; -using Serilog.Events; +using Serilog.Events; using System; using Log = PepperDash.Core.Debug; @@ -11,31 +10,62 @@ namespace PepperDash.Core.Logging { 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); From 6c710dd209841c1c43cca77b5574d55ba17769f7 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 26 Mar 2025 00:12:37 -0500 Subject: [PATCH 24/26] chore(force-patch): increment patch version From 8b3eda1d180d776fa0506f3afcbb7c4d25bcc890 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 26 Mar 2025 08:55:07 -0500 Subject: [PATCH 25/26] refactor: make messenger constructors more consistent Some constructors for messengers were taking Device rather than the specific type they needed. --- .../DeviceTypeExtensions/IChannelMessenger.cs | 4 +- .../DeviceTypeExtensions/IColorMessenger.cs | 2 +- .../DeviceTypeExtensions/IDPadMessenger.cs | 4 +- .../DeviceTypeExtensions/IDvrMessenger.cs | 4 +- .../IHasPowerMessenger.cs | 4 +- .../DeviceTypeExtensions/INumericMessenger.cs | 4 +- .../ISetTopBoxControlsMessenger.cs | 4 +- .../ITransportMessenger.cs | 4 +- .../IEssentialsRoomCombinerMessenger.cs | 2 +- .../IHasPowerControlWithFeedbackMessenger.cs | 2 +- .../IHasScheduleAwarenessMessenger.cs | 2 +- .../Messengers/IHumiditySensor.cs | 2 +- .../Messengers/ILevelControlsMessenger.cs | 2 +- .../Messengers/IMatrixRoutingMessenger.cs | 2 +- .../IProjectorScreenLiftControlMessenger.cs | 2 +- .../Messengers/IRunRouteActionMessenger.cs | 2 +- .../Messengers/ISelectableItemsMessenger.cs | 4 +- .../IShutdownPromptTimerMessenger.cs | 2 +- .../Messengers/ISwitchedOutputMessenger.cs | 2 +- .../Messengers/ITechPasswordMessenger.cs | 2 +- .../Messengers/ITemperatureSensorMessenger.cs | 2 +- .../Messengers/LightingBaseMessenger.cs | 2 +- .../Messengers/RoomEventScheduleMessenger.cs | 2 +- .../Messengers/ShadeBaseMessenger.cs | 2 +- .../Messengers/TwoWayDisplayBaseMessenger.cs | 6 +- .../MobileControlSystemController.cs | 101 +++++------------- 26 files changed, 59 insertions(+), 112 deletions(-) diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs index 3cc382f8..4ba89800 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IChannelMessenger.cs @@ -8,9 +8,9 @@ namespace PepperDash.Essentials.Room.MobileControl { private readonly IChannel channelDevice; - public IChannelMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + public IChannelMessenger(string key, string messagePath, IChannel device) : base(key, messagePath, device as IKeyName) { - channelDevice = device as IChannel; + channelDevice = device; } protected override void RegisterActions() diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs index 021f8a25..86df2590 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IColorMessenger.cs @@ -7,7 +7,7 @@ namespace PepperDash.Essentials.Room.MobileControl public class IColorMessenger : MessengerBase { private readonly IColor colorDevice; - public IColorMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + public IColorMessenger(string key, string messagePath, IColor device) : base(key, messagePath, device as IKeyName) { colorDevice = device as IColor; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs index 1233d0da..4af07703 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDPadMessenger.cs @@ -7,9 +7,9 @@ namespace PepperDash.Essentials.Room.MobileControl public class IDPadMessenger : MessengerBase { private readonly IDPad dpadDevice; - public IDPadMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + public IDPadMessenger(string key, string messagePath, IDPad device) : base(key, messagePath, device as IKeyName) { - dpadDevice = device as IDPad; + dpadDevice = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs index 2fd4679c..8e286979 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IDvrMessenger.cs @@ -7,9 +7,9 @@ namespace PepperDash.Essentials.Room.MobileControl public class IDvrMessenger : MessengerBase { private readonly IDvr dvrDevice; - public IDvrMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + public IDvrMessenger(string key, string messagePath, IDvr device) : base(key, messagePath, device as IKeyName) { - dvrDevice = device as IDvr; + dvrDevice = device; } protected override void RegisterActions() diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs index 8fe09e75..39ed0e6f 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/IHasPowerMessenger.cs @@ -7,9 +7,9 @@ namespace PepperDash.Essentials.Room.MobileControl public class IHasPowerMessenger : MessengerBase { private readonly IHasPowerControl powerDevice; - public IHasPowerMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + public IHasPowerMessenger(string key, string messagePath, IHasPowerControl device) : base(key, messagePath, device as IKeyName) { - powerDevice = device as IHasPowerControl; + powerDevice = device; } protected override void RegisterActions() diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs index 490b02b8..69b5bc9d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/INumericMessenger.cs @@ -7,9 +7,9 @@ namespace PepperDash.Essentials.Room.MobileControl public class INumericKeypadMessenger : MessengerBase { private readonly INumericKeypad keypadDevice; - public INumericKeypadMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + public INumericKeypadMessenger(string key, string messagePath, INumericKeypad device) : base(key, messagePath, device as IKeyName) { - keypadDevice = device as INumericKeypad; + keypadDevice = device; } protected override void RegisterActions() diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs index 16899110..0e7c227b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ISetTopBoxControlsMessenger.cs @@ -7,9 +7,9 @@ namespace PepperDash.Essentials.Room.MobileControl public class ISetTopBoxControlsMessenger : MessengerBase { private readonly ISetTopBoxControls stbDevice; - public ISetTopBoxControlsMessenger(string key, string messagePath, IKeyName device) : base(key, messagePath, device) + public ISetTopBoxControlsMessenger(string key, string messagePath, ISetTopBoxControls device) : base(key, messagePath, device as IKeyName) { - stbDevice = device as ISetTopBoxControls; + stbDevice = device; } protected override void RegisterActions() diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs index ee47491b..75f74418 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/DeviceTypeExtensions/ITransportMessenger.cs @@ -7,9 +7,9 @@ namespace PepperDash.Essentials.Room.MobileControl public class ITransportMessenger : MessengerBase { private readonly ITransport transportDevice; - public ITransportMessenger(string key, string messagePath, Device device) : base(key, messagePath, device) + public ITransportMessenger(string key, string messagePath, ITransport device) : base(key, messagePath, device as IKeyName) { - transportDevice = device as ITransport; + transportDevice = device; } protected override void RegisterActions() diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs index 9dc9eac7..a29d7b9e 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs @@ -13,7 +13,7 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly IEssentialsRoomCombiner _roomCombiner; public IEssentialsRoomCombinerMessenger(string key, string messagePath, IEssentialsRoomCombiner roomCombiner) - : base(key, messagePath, roomCombiner as Device) + : base(key, messagePath, roomCombiner as IKeyName) { _roomCombiner = roomCombiner; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs index 689b5102..7fb39c8c 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasPowerControlWithFeedbackMessenger.cs @@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly IHasPowerControlWithFeedback _powerControl; public IHasPowerControlWithFeedbackMessenger(string key, string messagePath, IHasPowerControlWithFeedback powerControl) - : base(key, messagePath, powerControl as Device) + : base(key, messagePath, powerControl as IKeyName) { _powerControl = powerControl; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs index 86529e1b..80481470 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHasScheduleAwarenessMessenger.cs @@ -12,7 +12,7 @@ namespace PepperDash.Essentials.AppServer.Messengers public IHasScheduleAwareness ScheduleSource { get; private set; } public IHasScheduleAwarenessMessenger(string key, IHasScheduleAwareness scheduleSource, string messagePath) - : base(key, messagePath, scheduleSource as Device) + : base(key, messagePath, scheduleSource as IKeyName) { ScheduleSource = scheduleSource ?? throw new ArgumentNullException("scheduleSource"); ScheduleSource.CodecSchedule.MeetingsListHasChanged += new EventHandler(CodecSchedule_MeetingsListHasChanged); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs index 0e500250..c44ec9ae 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IHumiditySensor.cs @@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly IHumiditySensor device; public IHumiditySensorMessenger(string key, IHumiditySensor device, string messagePath) - : base(key, messagePath, device as Device) + : base(key, messagePath, device as IKeyName) { this.device = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs index a476e9bc..4fd3515a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ILevelControlsMessenger.cs @@ -10,7 +10,7 @@ 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 Device) + public ILevelControlsMessenger(string key, string messagePath, ILevelControls device) : base(key, messagePath, device as IKeyName) { levelControlsDevice = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs index 4e403261..2a9669f1 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IMatrixRoutingMessenger.cs @@ -16,7 +16,7 @@ namespace PepperDash.Essentials.AppServer.Messengers public class IMatrixRoutingMessenger : MessengerBase { private readonly IMatrixRouting matrixDevice; - public IMatrixRoutingMessenger(string key, string messagePath, IMatrixRouting device) : base(key, messagePath, device as Device) + public IMatrixRoutingMessenger(string key, string messagePath, IMatrixRouting device) : base(key, messagePath, device as IKeyName) { matrixDevice = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs index af885639..a66a8586 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IProjectorScreenLiftControlMessenger.cs @@ -12,7 +12,7 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly IProjectorScreenLiftControl device; public IProjectorScreenLiftControlMessenger(string key, string messagePath, IProjectorScreenLiftControl screenLiftDevice) - : base(key, messagePath, screenLiftDevice as Device) + : base(key, messagePath, screenLiftDevice as IKeyName) { device = screenLiftDevice; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs index 98824530..8ca6668b 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IRunRouteActionMessenger.cs @@ -15,7 +15,7 @@ namespace PepperDash.Essentials.AppServer.Messengers public IRunRouteAction RoutingDevice { get; private set; } public RunRouteActionMessenger(string key, IRunRouteAction routingDevice, string messagePath) - : base(key, messagePath, routingDevice as Device) + : base(key, messagePath, routingDevice as IKeyName) { RoutingDevice = routingDevice ?? throw new ArgumentNullException("routingDevice"); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs index 187a82fb..f66ba53d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISelectableItemsMessenger.cs @@ -9,10 +9,10 @@ namespace PepperDash.Essentials.AppServer.Messengers public class ISelectableItemsMessenger : MessengerBase { private static readonly JsonSerializer serializer = new JsonSerializer { Converters = { new StringEnumConverter() } }; - private ISelectableItems itemDevice; + private readonly ISelectableItems itemDevice; private readonly string _propName; - public ISelectableItemsMessenger(string key, string messagePath, ISelectableItems device, string propName) : base(key, messagePath, device as Device) + public ISelectableItemsMessenger(string key, string messagePath, ISelectableItems device, string propName) : base(key, messagePath, device as IKeyName) { itemDevice = device; _propName = propName; diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs index a2e6a6f8..ca5fc3d3 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IShutdownPromptTimerMessenger.cs @@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly IShutdownPromptTimer _room; public IShutdownPromptTimerMessenger(string key, string messagePath, IShutdownPromptTimer room) - : base(key, messagePath, room as Device) + : base(key, messagePath, room as IKeyName) { _room = room; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs index cdd238e2..f49d189d 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ISwitchedOutputMessenger.cs @@ -11,7 +11,7 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly ISwitchedOutput device; public ISwitchedOutputMessenger(string key, ISwitchedOutput device, string messagePath) - : base(key, messagePath, device as Device) + : base(key, messagePath, device as IKeyName) { this.device = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs index 438fcef0..5b15d7ab 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITechPasswordMessenger.cs @@ -9,7 +9,7 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly ITechPassword _room; public ITechPasswordMessenger(string key, string messagePath, ITechPassword room) - : base(key, messagePath, room as Device) + : base(key, messagePath, room as IKeyName) { _room = room; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs index 8d1d5771..6f7371c1 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ITemperatureSensorMessenger.cs @@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly ITemperatureSensor device; public ITemperatureSensorMessenger(string key, ITemperatureSensor device, string messagePath) - : base(key, messagePath, device as Device) + : base(key, messagePath, device as IKeyName) { this.device = device; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs index 5c04491b..c8973aae 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/LightingBaseMessenger.cs @@ -11,7 +11,7 @@ namespace PepperDash.Essentials.AppServer.Messengers protected ILightingScenes Device { get; private set; } public ILightingScenesMessenger(string key, ILightingScenes device, string messagePath) - : base(key, messagePath, device as Device) + : base(key, messagePath, device as IKeyName) { Device = device ?? throw new ArgumentNullException("device"); Device.LightingSceneChange += new EventHandler(LightingDevice_LightingSceneChange); diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs index 122dc883..b8f5feff 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/RoomEventScheduleMessenger.cs @@ -14,7 +14,7 @@ namespace PepperDash.Essentials.AppServer.Messengers public RoomEventScheduleMessenger(string key, string messagePath, IRoomEventSchedule room) - : base(key, messagePath, room as Device) + : base(key, messagePath, room as IKeyName) { _room = room; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs index c5f27318..ff41670a 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/ShadeBaseMessenger.cs @@ -10,7 +10,7 @@ namespace PepperDash.Essentials.AppServer.Messengers private readonly IShadesOpenCloseStop device; public IShadesOpenCloseStopMessenger(string key, IShadesOpenCloseStop shades, string messagePath) - : base(key, messagePath, shades as Device) + : base(key, messagePath, shades as IKeyName) { device = shades; } diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs index 0fb5cbae..f0600dd5 100644 --- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs +++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/TwoWayDisplayBaseMessenger.cs @@ -9,12 +9,8 @@ namespace PepperDash.Essentials.AppServer.Messengers { private readonly TwoWayDisplayBase _display; - public TwoWayDisplayBaseMessenger(string key, string messagePath) : base(key, messagePath) - { - } - public TwoWayDisplayBaseMessenger(string key, string messagePath, TwoWayDisplayBase display) - : this(key, messagePath) + : base(key, messagePath, display) { _display = display; } diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs index 2a6d0321..145b3cbb 100644 --- a/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs +++ b/src/PepperDash.Essentials.MobileControl/MobileControlSystemController.cs @@ -566,18 +566,16 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is ISetTopBoxControls) + if (device is ISetTopBoxControls stbDevice) { this.LogVerbose( "Adding ISetTopBoxControlMessenger for {deviceKey}" - ); - - var dev = device as Device; + ); var messenger = new ISetTopBoxControlsMessenger( $"{device.Key}-stb-{Key}", $"/device/{device.Key}", - dev + stbDevice ); AddDefaultDeviceMessenger(messenger); @@ -585,18 +583,16 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is IChannel) + if (device is IChannel channelDevice) { this.LogVerbose( "Adding IChannelMessenger for {deviceKey}", device.Key - ); - - var dev = device as PepperDash.Core.Device; + ); var messenger = new IChannelMessenger( $"{device.Key}-channel-{Key}", $"/device/{device.Key}", - dev + channelDevice ); AddDefaultDeviceMessenger(messenger); @@ -604,16 +600,14 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is IColor) + if (device is IColor colorDevice) { - this.LogVerbose("Adding IColorMessenger for {deviceKey}", device.Key); - - var dev = device as PepperDash.Core.Device; + this.LogVerbose("Adding IColorMessenger for {deviceKey}", device.Key); var messenger = new IColorMessenger( $"{device.Key}-color-{Key}", $"/device/{device.Key}", - dev + colorDevice ); AddDefaultDeviceMessenger(messenger); @@ -621,16 +615,14 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is IDPad) + if (device is IDPad dPadDevice) { - this.LogVerbose("Adding IDPadMessenger for {deviceKey}", device.Key); - - var dev = device as PepperDash.Core.Device; + this.LogVerbose("Adding IDPadMessenger for {deviceKey}", device.Key); var messenger = new IDPadMessenger( $"{device.Key}-dPad-{Key}", $"/device/{device.Key}", - dev + dPadDevice ); AddDefaultDeviceMessenger(messenger); @@ -638,16 +630,14 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is INumericKeypad) + if (device is INumericKeypad nkDevice) { - this.LogVerbose("Adding INumericKeyapdMessenger for {deviceKey}", device.Key); - - var dev = device as PepperDash.Core.Device; + this.LogVerbose("Adding INumericKeyapdMessenger for {deviceKey}", device.Key); var messenger = new INumericKeypadMessenger( $"{device.Key}-numericKeypad-{Key}", $"/device/{device.Key}", - dev + nkDevice ); AddDefaultDeviceMessenger(messenger); @@ -655,16 +645,14 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is IHasPowerControl) + if (device is IHasPowerControl pcDevice) { - this.LogVerbose("Adding IHasPowerControlMessenger for {deviceKey}", device.Key); - - var dev = device as PepperDash.Core.Device; + this.LogVerbose("Adding IHasPowerControlMessenger for {deviceKey}", device.Key); var messenger = new IHasPowerMessenger( $"{device.Key}-powerControl-{Key}", $"/device/{device.Key}", - dev + pcDevice ); AddDefaultDeviceMessenger(messenger); @@ -689,18 +677,16 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is ITransport) + if (device is ITransport transportDevice) { this.LogVerbose( "Adding ITransportMessenger for {deviceKey}", device.Key - ); + ); - var dev = device as PepperDash.Core.Device; - - var messenger = new IChannelMessenger( + var messenger = new ITransportMessenger( $"{device.Key}-transport-{Key}", $"/device/{device.Key}", - dev + transportDevice ); AddDefaultDeviceMessenger(messenger); @@ -708,14 +694,14 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is IHasCurrentSourceInfoChange) + if (device is IHasCurrentSourceInfoChange csiChange) { this.LogVerbose("Adding IHasCurrentSourceInfoMessenger for {deviceKey}", device.Key); var messenger = new IHasCurrentSourceInfoMessenger( $"{device.Key}-currentSource-{Key}", $"/device/{device.Key}", - device as IHasCurrentSourceInfoChange + csiChange ); AddDefaultDeviceMessenger(messenger); @@ -723,7 +709,7 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is ISwitchedOutput) + if (device is ISwitchedOutput switchedDevice) { this.LogVerbose( "Adding ISwitchedOutputMessenger for {deviceKey}", device.Key @@ -731,7 +717,7 @@ namespace PepperDash.Essentials var messenger = new ISwitchedOutputMessenger( $"{device.Key}-switchedOutput-{Key}", - device as ISwitchedOutput, + switchedDevice, $"/device/{device.Key}" ); @@ -773,40 +759,6 @@ namespace PepperDash.Essentials messengerAdded = true; } - // This will work if TKey and TSelector are both string types. - // Otherwise plugin device needs to instantiate ISelectableItemsMessenger and add it to the controller. - //if (device is IHasInputs inputs) - //{ - // this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); - - // var messenger = new ISelectableItemsMessenger( - // $"{device.Key}-inputs-{Key}", - // $"/device/{device.Key}", - // inputs.Inputs, - // "inputs" - // ); - - // AddDefaultDeviceMessenger(messenger); - - // messengerAdded = true; - //} - - //if (device is IHasInputs byteIntInputs) - //{ - // this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); - - // var messenger = new ISelectableItemsMessenger( - // $"{device.Key}-inputs-{Key}", - // $"/device/{device.Key}", - // byteIntInputs.Inputs, - // "inputs" - // ); - - // AddDefaultDeviceMessenger(messenger); - - // messengerAdded = true; - //} - if (device is IHasInputs stringInputs) { this.LogVerbose("Adding InputsMessenger for {deviceKey}", device.Key); @@ -855,7 +807,6 @@ namespace PepperDash.Essentials messengerAdded = true; } - if (device is IMatrixRouting matrix) { this.LogVerbose( From 90aa4a5d6261ea34c2a35233dbb9f7b569148375 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 26 Mar 2025 08:55:55 -0500 Subject: [PATCH 26/26] feat: remove IHasInputs interface This has been replaced by the `IHasInputs` interface and was marked to be removed in the 2.0.0 release. --- .../DeviceTypeInterfaces/IHasInputs.cs | 20 ------------------- 1 file changed, 20 deletions(-) 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 ///