using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using Crestron.SimplSharp; using Crestron.SimplSharp.CrestronIO; using Crestron.SimplSharp.Reflection; using Crestron.SimplSharpPro.CrestronThread; using Crestron.SimplSharp.CrestronWebSocketClient; using Crestron.SimplSharpPro; using Crestron.SimplSharp.Net.Http; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using PepperDash.Core; using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Config; using PepperDash.Essentials.Room.MobileControl; using PepperDash.Essentials.AppServer.Messengers; namespace PepperDash.Essentials { public class MobileControlSystemController : EssentialsDevice { WebSocketClient WSClient; //bool LinkUp; /// /// Prevents post operations from stomping on each other and getting lost /// CEvent PostLockEvent = new CEvent(true, true); CEvent RegisterLockEvent = new CEvent(true, true); public MobileControlConfig Config { get; private set; } Dictionary ActionDictionary = new Dictionary(StringComparer.InvariantCultureIgnoreCase); Dictionary PushedActions = new Dictionary(); public ConfigMessenger ConfigMessenger { get; private set; } CTimer ServerHeartbeatCheckTimer; long ServerHeartbeatInterval = 20000; CTimer ServerReconnectTimer; long ServerReconnectInterval = 5000; DateTime LastAckMessage; public string SystemUuid; List RoomBridges = new List(); long ButtonHeartbeatInterval = 1000; /// /// Used for tracking HTTP debugging /// bool HttpDebugEnabled; /// /// /// /// /// /// public MobileControlSystemController(string key, string name, MobileControlConfig config) : base(key, name) { Config = config; SystemUuid = ConfigReader.ConfigObject.SystemUuid; Debug.Console(0, this, "Mobile UI controller initializing for server:{0}", config.ServerUrl); 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 => ConnectWebsocketClient(), "mobileconnect", "Forces connect of websocket", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(s => CleanUpWebsocketClient(), "mobiledisco", "Disconnects websocket", ConsoleAccessLevelEnum.AccessOperator); CrestronConsole.AddNewConsoleCommand(s => ParseStreamRx(s), "mobilesimulateaction", "Simulates a message from the server", ConsoleAccessLevelEnum.AccessOperator); CrestronEnvironment.ProgramStatusEventHandler += new ProgramStatusEventHandler(CrestronEnvironment_ProgramStatusEventHandler); CrestronEnvironment.EthernetEventHandler += new EthernetEventHandler(CrestronEnvironment_EthernetEventHandler); // Config Messenger var cmKey = Key + "-config"; ConfigMessenger = new ConfigMessenger(cmKey, "/config"); ConfigMessenger.RegisterWithAppServer(this); } /// /// If config rooms is empty or null then go /// /// public override bool CustomActivate() { if (ConfigReader.ConfigObject.Rooms == null || ConfigReader.ConfigObject.Rooms.Count == 0) { Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Config contains no rooms. Registering with Server."); RegisterSystemToServer(); } return base.CustomActivate(); } /// /// /// /// void CrestronEnvironment_EthernetEventHandler(EthernetEventArgs args) { Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Ethernet status change, port {0}: {1}", args.EthernetAdapter, args.EthernetEventType); if (args.EthernetEventType == eEthernetEventType.LinkDown && WSClient != null && args.EthernetAdapter == WSClient.EthernetAdapter) { CleanUpWebsocketClient(); } } /// /// Sends message to server to indicate the system is shutting down /// /// void CrestronEnvironment_ProgramStatusEventHandler(eProgramStatusEventType programEventType) { if (programEventType == eProgramStatusEventType.Stopping && WSClient != null && WSClient.Connected) { CleanUpWebsocketClient(); } } public void PrintActionDictionaryPaths(object o) { Debug.Console(0, this, "ActionDictionary Contents:"); foreach (var item in ActionDictionary) { Debug.Console(0, this, "{0}", item.Key); } } /// /// Adds an action to the dictionary /// /// The path of the API command /// The action to be triggered by the commmand public void AddAction(string key, object action) { if (!ActionDictionary.ContainsKey(key)) { ActionDictionary.Add(key, action); } else { Debug.Console(1, this, "Cannot add action with key '{0}' because key already exists in ActionDictionary.", key); } } /// /// Removes an action from the dictionary /// /// public void RemoveAction(string key) { if (ActionDictionary.ContainsKey(key)) ActionDictionary.Remove(key); } /// /// /// /// public void AddBridge(MobileControlBridgeBase bridge) { RoomBridges.Add(bridge); var b = bridge as IDelayedConfiguration; if (b != null) { Debug.Console(0, this, "Adding room bridge with delayed configuration"); b.ConfigurationIsReady += new EventHandler(bridge_ConfigurationIsReady); } else { Debug.Console(0, this, "Adding room bridge and sending configuration"); //SystemUuid = ConfigReader.ConfigObject.SystemUuid; RegisterSystemToServer(); } } /// /// /// /// /// void bridge_ConfigurationIsReady(object sender, EventArgs e) { Debug.Console(1, this, "Bridge ready. Registering"); //SystemUuid = ConfigReader.ConfigObject.SystemUuid; // send the configuration object to the server RegisterSystemToServer(); } /// /// /// /// void ReconnectToServerTimerCallback(object o) { RegisterSystemToServer(); } /// /// Verifies system connection with servers /// /// void AuthorizeSystem(string code) { //SystemUuid = ConfigReader.ConfigObject.SystemUuid; if (string.IsNullOrEmpty(SystemUuid)) { CrestronConsole.ConsoleCommandResponse("System does not have a UUID. Please ensure proper portal-format configuration is loaded and restart."); return; } if (string.IsNullOrEmpty(code)) { CrestronConsole.ConsoleCommandResponse("Please enter a user code to authorize a system"); return; } var req = new HttpClientRequest(); string url = string.Format("http://{0}/api/system/grantcode/{1}/{2}", Config.ServerUrl, code, SystemUuid); Debug.Console(0, this, "Authorizing to: {0}", url); if (string.IsNullOrEmpty(Config.ServerUrl)) { CrestronConsole.ConsoleCommandResponse("Config URL address is not set. Check portal configuration"); return; } try { req.Url.Parse(url); new HttpClient().DispatchAsync(req, (r, e) => { CheckHttpDebug(r, e); if (e == HTTP_CALLBACK_ERROR.COMPLETED) { if (r.Code == 200) { Debug.Console(0, "System authorized, sending config."); #warning This registration may need to wait for config ready. Maybe. RegisterSystemToServer(); } else if (r.Code == 404) { if (r.ContentString.Contains("codeNotFound")) { Debug.Console(0, "Authorization failed, code not found for system UUID {0}", SystemUuid); } else if (r.ContentString.Contains("uuidNotFound")) { Debug.Console(0, "Authorization failed, uuid {0} not found. Check Essentials configuration is correct", SystemUuid); } } else { Debug.Console(0, "Authorization failed, code {0}: {1}", r.Code, r.ContentString); } } else Debug.Console(0, this, "Error {0} in authorizing system", e); }); } catch (Exception e) { Debug.Console(0, this, "Error in authorizing: {0}", e); } } /// /// Dumps info in response to console command. /// void ShowInfo() { var url = Config != null ? Config.ServerUrl : "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 = WSClient == null ? "No client" : (WSClient.Connected ? "Yes" : "No"); var secSinceLastAck = DateTime.Now - LastAckMessage; CrestronConsole.ConsoleCommandResponse(@"Mobile Control 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); } /// /// Registers the room with the server /// /// URL of the server, including the port number, if not 80. Format: "serverUrlOrIp:port" void RegisterSystemToServer() { ConnectWebsocketClient(); } /// /// Connects the Websocket Client /// /// void ConnectWebsocketClient() { Debug.Console(1, this, "Initializing Stream client to server."); if (WSClient != null) { Debug.Console(1, this, "Cleaning up previous socket"); CleanUpWebsocketClient(); } WSClient = new WebSocketClient(); WSClient.URL = string.Format("wss://{0}/system/join/{1}", Config.ServerUrl, this.SystemUuid); WSClient.ConnectionCallBack = Websocket_ConnectCallback; WSClient.ConnectAsync(); } /// /// /// /// /// int Websocket_ConnectCallback(WebSocketClient.WEBSOCKET_RESULT_CODES code) { if (code == WebSocketClient.WEBSOCKET_RESULT_CODES.WEBSOCKET_CLIENT_SUCCESS) { StopServerReconnectTimer(); Debug.Console(1, this, "Websocket connected"); WSClient.DisconnectCallBack = Websocket_DisconnectCallback; WSClient.SendCallBack = Websocket_SendCallback; WSClient.ReceiveCallBack = Websocket_ReceiveCallback; WSClient.ReceiveAsync(); SendMessageObjectToServer(new { type = "hello" }); } else { if (code == WebSocketClient.WEBSOCKET_RESULT_CODES.WEBSOCKET_CLIENT_HTTP_HANDSHAKE_TOKEN_ERROR) { // This is the case when app server is running behind a websever and app server is down Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Web socket connection failed. Check that app server is running behind web server"); } else if (code == WebSocketClient.WEBSOCKET_RESULT_CODES.WEBSOCKET_CLIENT_SOCKET_CONNECTION_FAILED) { // this will be the case when webserver is unreachable Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Web socket connection failed"); } else { Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Web socket connection failure: {0}", code); } StartServerReconnectTimer(); } return 0; } /// /// After a "hello" from the server, sends config and stuff /// void SendInitialMessage() { Debug.Console(1, this, "Sending initial join message"); var confObject = ConfigReader.ConfigObject; confObject.Info.RuntimeInfo.AppName = Assembly.GetExecutingAssembly().GetName().Name; var version = Assembly.GetExecutingAssembly().GetName().Version; confObject.Info.RuntimeInfo.AssemblyVersion = string.Format("{0}.{1}.{2}", version.Major, version.Minor, version.Build); var msg = new { type = "join", content = new { config = confObject } }; SendMessageObjectToServer(msg); } /// /// Sends any object type to server /// /// public void SendMessageObjectToServer(object o) { SendMessageToServer(JObject.FromObject(o)); } /// /// Sends a message to the server from a room /// /// room from which the message originates /// object to be serialized and sent in post body public void SendMessageToServer(JObject o) { if (WSClient != null && WSClient.Connected) { string message = JsonConvert.SerializeObject(o, Formatting.None, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); if (!message.Contains("/system/heartbeat")) Debug.Console(1, this, "Message TX: {0}", message); //else // Debug.Console(1, this, "TX messages contains /system/heartbeat"); var messageBytes = System.Text.Encoding.UTF8.GetBytes(message); var result = WSClient.Send(messageBytes, (uint)messageBytes.Length, WebSocketClient.WEBSOCKET_PACKET_TYPES.LWS_WS_OPCODE_07__TEXT_FRAME); if (result != WebSocketClient.WEBSOCKET_RESULT_CODES.WEBSOCKET_CLIENT_SUCCESS) { Debug.Console(1, this, "Socket send result error: {0}", result); } } else if (WSClient == null) { Debug.Console(1, this, "Cannot send. Not connected."); } } /// /// Disconnects the SSE Client and stops the heartbeat timer /// /// void CleanUpWebsocketClient() { Debug.Console(1, this, "Disconnecting websocket"); if (WSClient != null) { WSClient.SendCallBack = null; WSClient.ReceiveCallBack = null; WSClient.ConnectionCallBack = null; WSClient.DisconnectCallBack = null; if (WSClient.Connected) { WSClient.Disconnect(); } WSClient = null; } } /// /// /// /// /// void StartServerReconnectTimer() { StopServerReconnectTimer(); ServerReconnectTimer = new CTimer(ReconnectToServerTimerCallback, ServerReconnectInterval); Debug.Console(1, this, "Reconnect Timer Started."); } /// /// Does what it says /// void StopServerReconnectTimer() { if (ServerReconnectTimer != null) { ServerReconnectTimer.Stop(); ServerReconnectTimer = null; } } /// /// Executes when we don't get a heartbeat message in time. Triggers reconnect. /// /// For CTimer callback. Not used void HeartbeatExpiredTimerCallback(object o) { Debug.Console(1, this, "Heartbeat Timer Expired."); if (ServerHeartbeatCheckTimer != null) { ServerHeartbeatCheckTimer.Stop(); ServerHeartbeatCheckTimer = null; } CleanUpWebsocketClient(); StartServerReconnectTimer(); } /// /// /// /// /// void ResetOrStartHearbeatTimer() { if (ServerHeartbeatCheckTimer == null) { ServerHeartbeatCheckTimer = new CTimer(HeartbeatExpiredTimerCallback, null, ServerHeartbeatInterval, ServerHeartbeatInterval); Debug.Console(1, this, "Heartbeat Timer Started."); } else { ServerHeartbeatCheckTimer.Reset(ServerHeartbeatInterval, ServerHeartbeatInterval); } } /// /// Waits two and goes again /// void ReconnectStreamClient() { new CTimer(o => ConnectWebsocketClient(), 2000); } /// /// /// /// /// int Websocket_DisconnectCallback(WebSocketClient.WEBSOCKET_RESULT_CODES code, object o) { Debug.Console(1, this, Debug.ErrorLogLevel.Warning, "Websocket disconnected with code: {0}", code); if (ServerHeartbeatCheckTimer != null) ServerHeartbeatCheckTimer.Stop(); // Start the reconnect timer StartServerReconnectTimer(); return 0; } /// /// Resets reconnect timer and updates usercode /// /// void HandleHeartBeat(JToken content) { SendMessageToServer(JObject.FromObject(new { type = "/system/heartbeatAck" })); var code = content["userCode"]; if(code != null) { foreach (var b in RoomBridges) { b.SetUserCode(code.Value()); } } ResetOrStartHearbeatTimer(); } /// /// Outputs debug info when enabled /// /// /// /// void CheckHttpDebug(HttpClientResponse r, HTTP_CALLBACK_ERROR e) { if (HttpDebugEnabled) { try { Debug.Console(0, this, "------ Begin HTTP Debug ---------------------------------------"); if (r != null) { Debug.Console(0, this, "HTTP Response URL: {0}", r.ResponseUrl != null ? r.ResponseUrl.ToString() : "NONE"); Debug.Console(0, this, "HTTP Response code: {0}", r.Code); Debug.Console(0, this, "HTTP Response content: \r{0}", r.ContentString); } else { Debug.Console(0, this, "No HTTP response"); } Debug.Console(0, this, "HTTP Response 'error' {0}", e); Debug.Console(0, this, "------ End HTTP Debug -----------------------------------------"); } catch (Exception ex) { Debug.Console(0, this, "HttpDebugError: {0}", ex); } } } /// /// /// /// /// /// /// int Websocket_ReceiveCallback(byte[] data, uint length, WebSocketClient.WEBSOCKET_PACKET_TYPES opcode, WebSocketClient.WEBSOCKET_RESULT_CODES err) { if (opcode == WebSocketClient.WEBSOCKET_PACKET_TYPES.LWS_WS_OPCODE_07__TEXT_FRAME) { var rx = System.Text.Encoding.UTF8.GetString(data, 0, (int)length); if (rx.Length > 0) ParseStreamRx(rx); WSClient.ReceiveAsync(); } else if (opcode == WebSocketClient.WEBSOCKET_PACKET_TYPES.LWS_WS_OPCODE_07__CLOSE) { Debug.Console(1, this, "Websocket disconnect received from remote"); CleanUpWebsocketClient(); } else { Debug.Console(1, this, "websocket rx opcode/err {0}/{1}", opcode, err); WSClient.ReceiveAsync(); } return 0; } /// /// Callback to catch possible errors in sending via the websocket /// /// /// int Websocket_SendCallback(Crestron.SimplSharp.CrestronWebSocketClient.WebSocketClient.WEBSOCKET_RESULT_CODES result) { if(result != WebSocketClient.WEBSOCKET_RESULT_CODES.WEBSOCKET_CLIENT_SUCCESS) Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "SendCallback questionable result: {0}", result); return 1; } /// /// /// /// /// void ParseStreamRx(string message) { if(string.IsNullOrEmpty(message)) return; if (!message.Contains("/system/heartbeat")) { Debug.Console(1, this, "Message RX: {0}", message); } else { LastAckMessage = DateTime.Now; } try { var messageObj = JObject.Parse(message); var type = messageObj["type"].Value(); if (type == "hello") { SendInitialMessage(); ResetOrStartHearbeatTimer(); } else if (type == "/system/heartbeat") { HandleHeartBeat(messageObj["content"]); } else if (type == "raw") { var wrapper = messageObj["content"].ToObject(); DeviceJsonApi.DoDeviceAction(wrapper); } else if (type == "close") { Debug.Console(1, this, "Received close message from server."); // DisconnectWebsocketClient(); if (ServerHeartbeatCheckTimer != null) ServerHeartbeatCheckTimer.Stop(); } else { // Check path against Action dictionary if (ActionDictionary.ContainsKey(type)) { var action = ActionDictionary[type]; if (action is Action) { (action as Action)(); } else if (action is PressAndHoldAction) { var stateString = messageObj["content"]["state"].Value(); // Look for a button press event if (!string.IsNullOrEmpty(stateString)) { switch (stateString) { case "true": { if (!PushedActions.ContainsKey(type)) { PushedActions.Add(type, new CTimer(o => { (action as PressAndHoldAction)(false); PushedActions.Remove(type); }, null, ButtonHeartbeatInterval, ButtonHeartbeatInterval)); } // Maybe add an else to reset the timer break; } case "held": { if (PushedActions.ContainsKey(type)) { PushedActions[type].Reset(ButtonHeartbeatInterval, ButtonHeartbeatInterval); } return; } case "false": { if (PushedActions.ContainsKey(type)) { PushedActions[type].Stop(); PushedActions.Remove(type); } break; } } (action as PressAndHoldAction)(stateString == "true"); } } else if (action is Action) { var stateString = messageObj["content"]["state"].Value(); if (!string.IsNullOrEmpty(stateString)) { (action as Action)(stateString == "true"); } } else if (action is Action) { (action as Action)(messageObj["content"]["value"].Value()); } else if (action is Action) { (action as Action)(messageObj["content"]["value"].Value()); } else if (action is Action) { (action as Action)(messageObj["content"] .ToObject()); } } else { Debug.Console(1, this, "-- Warning: Incoming message has no registered handler"); } } } catch (Exception err) { //Debug.Console(1, "SseMessageLengthBeforeFailureCount: {0}", SseMessageLengthBeforeFailureCount); //SseMessageLengthBeforeFailureCount = 0; Debug.Console(1, this, "Unable to parse message: {0}", err); } } 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]; if (tokens[0].ToLower() == "get") { var resp = new HttpClient().Get(url); CrestronConsole.ConsoleCommandResponse("RESPONSE:\r{0}\r\r", resp); } else if (tokens[0].ToLower() == "post") { var resp = new HttpClient().Post(url, new byte[] { }); CrestronConsole.ConsoleCommandResponse("RESPONSE:\r{0}\r\r", resp); } else { CrestronConsole.ConsoleCommandResponse("Only get or post supported\r"); PrintTestHttpRequestUsage(); } } 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); } } } void PrintTestHttpRequestUsage() { CrestronConsole.ConsoleCommandResponse("Usage: mobilehttprequest:N get/post url\r"); } } public class MobileControlSystemControllerFactory : EssentialsDeviceFactory { public MobileControlSystemControllerFactory() { TypeNames = new List() { "appserver" }; } public override EssentialsDevice BuildDevice(DeviceConfig dc) { Debug.Console(1, "Factory Attempting to create new MobileControlSystemController Device"); var props = JsonConvert.DeserializeObject(dc.Properties.ToString()); return new MobileControlSystemController(dc.Key, dc.Name, props); } } }