From f6fdc140598a16362961817ac3ca3fa05da03b2f Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Mon, 24 Mar 2025 22:28:27 -0500 Subject: [PATCH] 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