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