mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 10:58:28 +00:00
1875 lines
No EOL
63 KiB
C#
1875 lines
No EOL
63 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
using System.Timers;
|
|
using Crestron.SimplSharp;
|
|
using Crestron.SimplSharp.CrestronIO;
|
|
using Crestron.SimplSharp.Net.Http;
|
|
using Crestron.SimplSharp.WebScripting;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using PepperDash.Core;
|
|
using PepperDash.Core.Logging;
|
|
using PepperDash.Essentials.AppServer;
|
|
using PepperDash.Essentials.AppServer.Messengers;
|
|
using PepperDash.Essentials.Core;
|
|
using PepperDash.Essentials.Core.Config;
|
|
using PepperDash.Essentials.Core.DeviceTypeInterfaces;
|
|
using PepperDash.Essentials.Core.Monitoring;
|
|
using PepperDash.Essentials.Core.Queues;
|
|
using PepperDash.Essentials.Core.Web;
|
|
using PepperDash.Essentials.RoomBridges;
|
|
using PepperDash.Essentials.Services;
|
|
using PepperDash.Essentials.WebApiHandlers;
|
|
using PepperDash.Essentials.WebSocketServer;
|
|
using WebSocketSharp;
|
|
|
|
namespace PepperDash.Essentials
|
|
{
|
|
/// <summary>
|
|
/// Represents a MobileControlSystemController
|
|
/// </summary>
|
|
public class MobileControlSystemController : EssentialsDevice, IMobileControl
|
|
{
|
|
private bool _initialized = false;
|
|
private const long ServerReconnectInterval = 5000;
|
|
private const long PingInterval = 25000;
|
|
|
|
private readonly Dictionary<string, List<IMobileControlAction>> _actionDictionary =
|
|
new Dictionary<string, List<IMobileControlAction>>(
|
|
StringComparer.InvariantCultureIgnoreCase
|
|
);
|
|
|
|
/// <summary>
|
|
/// Actions
|
|
/// </summary>
|
|
public ReadOnlyDictionary<string, List<IMobileControlAction>> ActionDictionary => new ReadOnlyDictionary<string, List<IMobileControlAction>>(_actionDictionary);
|
|
|
|
private readonly GenericQueue _receiveQueue;
|
|
private readonly List<MobileControlBridgeBase> _roomBridges =
|
|
new List<MobileControlBridgeBase>();
|
|
|
|
private readonly Dictionary<string, IMobileControlMessenger> _messengers =
|
|
new Dictionary<string, IMobileControlMessenger>();
|
|
|
|
private readonly Dictionary<string, IMobileControlMessenger> _defaultMessengers =
|
|
new Dictionary<string, IMobileControlMessenger>();
|
|
|
|
/// <summary>
|
|
/// Get the custom messengers
|
|
/// </summary>
|
|
public ReadOnlyDictionary<string, IMobileControlMessenger> Messengers => new ReadOnlyDictionary<string, IMobileControlMessenger>(_messengers);
|
|
|
|
/// <summary>
|
|
/// Get the default messengers
|
|
/// </summary>
|
|
public ReadOnlyDictionary<string, IMobileControlMessenger> DefaultMessengers => new ReadOnlyDictionary<string, IMobileControlMessenger>(_defaultMessengers);
|
|
|
|
private readonly GenericQueue _transmitToServerQueue;
|
|
|
|
private readonly GenericQueue _transmitToClientsQueue;
|
|
|
|
private bool _disableReconnect;
|
|
private WebSocket _wsClient2;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the ApiService
|
|
/// </summary>
|
|
public MobileControlApiService ApiService { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Get Room Bridges associated with this controller
|
|
/// </summary>
|
|
public List<MobileControlBridgeBase> RoomBridges => _roomBridges;
|
|
|
|
private readonly MobileControlWebsocketServer _directServer;
|
|
|
|
/// <summary>
|
|
/// Get the Direct Server instance associated with this controller
|
|
/// </summary>
|
|
public MobileControlWebsocketServer DirectServer => _directServer;
|
|
|
|
private readonly object _wsCriticalSection = new();
|
|
|
|
/// <summary>
|
|
/// Gets or sets the SystemUrl
|
|
/// </summary>
|
|
public string SystemUrl; //set only from SIMPL Bridge!
|
|
|
|
/// <summary>
|
|
/// True if the Mobile Control Edge Server Websocket is connected
|
|
/// </summary>
|
|
public bool Connected => _wsClient2 != null && _wsClient2.IsAlive;
|
|
|
|
private IEssentialsRoomCombiner _roomCombiner;
|
|
|
|
/// <summary>
|
|
/// Gets the SystemUuid from configuration or SIMPL Bridge
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
this.LogWarning(
|
|
"No system_url value defined in config. Checking for value from SIMPL Bridge."
|
|
);
|
|
|
|
if (string.IsNullOrEmpty(SystemUrl))
|
|
{
|
|
this.LogError(
|
|
"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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the ApiOnlineAndAuthorized
|
|
/// </summary>
|
|
public BoolFeedback ApiOnlineAndAuthorized { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Used for tracking HTTP debugging
|
|
/// </summary>
|
|
private bool _httpDebugEnabled;
|
|
|
|
private bool _isAuthorized;
|
|
|
|
/// <summary>
|
|
/// Tracks if the system is authorized to the API server
|
|
/// </summary>
|
|
public bool IsAuthorized
|
|
{
|
|
get { return _isAuthorized; }
|
|
private set
|
|
{
|
|
if (value == _isAuthorized)
|
|
return;
|
|
|
|
_isAuthorized = value;
|
|
ApiOnlineAndAuthorized.FireUpdate();
|
|
}
|
|
}
|
|
|
|
private DateTime _lastAckMessage;
|
|
|
|
/// <summary>
|
|
/// Gets the LastAckMessage timestamp
|
|
/// </summary>
|
|
public DateTime LastAckMessage => _lastAckMessage;
|
|
|
|
private Timer _pingTimer;
|
|
|
|
private Timer _serverReconnectTimer;
|
|
private LogLevel _wsLogLevel = LogLevel.Error;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="MobileControlSystemController"/> class.
|
|
/// </summary>
|
|
/// <param name="key">The unique key for this controller.</param>
|
|
/// <param name="name">The name of the controller.</param>
|
|
/// <param name="config">The configuration settings for the controller.</param>
|
|
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",
|
|
System.Threading.ThreadPriority.Highest,
|
|
100
|
|
);
|
|
|
|
// The queue that will collect the outgoing messages in the order they are received
|
|
_transmitToServerQueue = new GenericQueue(
|
|
key + "-txqueue",
|
|
System.Threading.ThreadPriority.Highest,
|
|
100
|
|
);
|
|
|
|
if (Config.DirectServer != null && Config.DirectServer.EnableDirectServer)
|
|
{
|
|
_directServer = new MobileControlWebsocketServer(
|
|
Key + "-directServer",
|
|
Config.DirectServer.Port,
|
|
this
|
|
);
|
|
DeviceManager.AddDevice(_directServer);
|
|
|
|
_transmitToClientsQueue = new GenericQueue(
|
|
key + "-clienttxqueue",
|
|
System.Threading.ThreadPriority.Highest,
|
|
100
|
|
);
|
|
}
|
|
|
|
Host = config.ServerUrl;
|
|
if (!Host.StartsWith("http"))
|
|
{
|
|
Host = "https://" + Host;
|
|
}
|
|
|
|
ApiService = new MobileControlApiService(Host);
|
|
|
|
this.LogInformation(
|
|
"Mobile UI controller initializing for server:{0}",
|
|
config.ServerUrl
|
|
);
|
|
|
|
if (Global.Platform == eDevicePlatform.Appliance)
|
|
{
|
|
AddConsoleCommands();
|
|
}
|
|
|
|
AddPreActivationAction(() => LinkSystemMonitorToAppServer());
|
|
|
|
AddPreActivationAction(() =>
|
|
{
|
|
var actionMessenger = new DeviceActionMessenger("deviceActionMessenger-" + Key, "/action");
|
|
AddDeviceMessenger(actionMessenger);
|
|
});
|
|
|
|
AddPreActivationAction(() => AddWebApiPaths());
|
|
|
|
AddPreActivationAction(() =>
|
|
{
|
|
_roomCombiner = DeviceManager.AllDevices.OfType<IEssentialsRoomCombiner>().FirstOrDefault();
|
|
|
|
if (_roomCombiner == null)
|
|
return;
|
|
|
|
_roomCombiner.RoomCombinationScenarioChanged += OnRoomCombinationScenarioChanged;
|
|
});
|
|
|
|
CrestronEnvironment.ProgramStatusEventHandler +=
|
|
CrestronEnvironment_ProgramStatusEventHandler;
|
|
|
|
ApiOnlineAndAuthorized = new BoolFeedback("apiOnlineAndAuthorized", () =>
|
|
{
|
|
if (_wsClient2 == null)
|
|
return false;
|
|
|
|
return _wsClient2.IsAlive && IsAuthorized;
|
|
});
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void AddDefaultMessengersForDevice(EssentialsDevice device, IEnumerable<Type> interfaces = null)
|
|
{
|
|
var interfaceSet = interfaces != null ? new HashSet<Type>(interfaces) : [.. device.GetType().GetInterfaces()];
|
|
var messengerAdded = false;
|
|
|
|
foreach (var entry in MessengerFactoryRegistry.Entries)
|
|
{
|
|
if (interfaceSet != null && !interfaceSet.Contains(entry.InterfaceType))
|
|
continue;
|
|
|
|
if (!entry.Matches(device))
|
|
continue;
|
|
|
|
var messenger = entry.Factory(device, $"/device/{device.Key}", Key);
|
|
|
|
if (messenger == null)
|
|
continue;
|
|
|
|
AddDefaultDeviceMessenger(messenger);
|
|
messengerAdded = true;
|
|
}
|
|
|
|
if (!messengerAdded)
|
|
{
|
|
this.LogDebug("Adding GenericMessenger for {deviceKey}", device.Key);
|
|
AddDefaultDeviceMessenger(new GenericMessenger(
|
|
$"{device.Key}-{Key}-generic",
|
|
device,
|
|
$"/device/{device.Key}"));
|
|
}
|
|
}
|
|
|
|
private void AddWebApiPaths()
|
|
{
|
|
var apiServer = DeviceManager
|
|
.AllDevices.OfType<EssentialsWebApi>()
|
|
.FirstOrDefault(d => d.Key == "essentialsWebApi");
|
|
|
|
if (apiServer == null)
|
|
{
|
|
this.LogWarning("No API Server available");
|
|
return;
|
|
}
|
|
|
|
// TODO: Add routes for the rest of the MC console commands
|
|
var routes = new List<HttpCwsRoute>
|
|
{
|
|
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;
|
|
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
$"Connecting to MC API server"
|
|
);
|
|
|
|
ConnectWebsocketClient();
|
|
},
|
|
"mobileconnect",
|
|
"Forces connect of websocket",
|
|
ConsoleAccessLevelEnum.AccessOperator
|
|
);
|
|
|
|
CrestronConsole.AddNewConsoleCommand(
|
|
s =>
|
|
{
|
|
_disableReconnect = true;
|
|
|
|
CleanUpWebsocketClient();
|
|
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
$"Disonnected from MC API server"
|
|
);
|
|
},
|
|
"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
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Config
|
|
/// </summary>
|
|
public MobileControlConfig Config { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the Host
|
|
/// </summary>
|
|
public string Host { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Gets the configured Client App URL
|
|
/// </summary>
|
|
public string ClientAppUrl => Config.ClientAppUrl;
|
|
|
|
private void OnRoomCombinationScenarioChanged(
|
|
object sender,
|
|
EventArgs eventArgs
|
|
)
|
|
{
|
|
SendMessageObject(new MobileControlMessage { Type = "/system/roomCombinationChanged" });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a device messenger exists for the given key.
|
|
/// </summary>
|
|
public bool CheckForDeviceMessenger(string key)
|
|
{
|
|
return _messengers.ContainsKey(key);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add the provided messenger to the messengers collection
|
|
/// </summary>
|
|
public void AddDeviceMessenger(IMobileControlMessenger messenger)
|
|
{
|
|
if (_messengers.ContainsKey(messenger.Key))
|
|
{
|
|
this.LogWarning("Messenger with key {messengerKey} already added", messenger.Key);
|
|
return;
|
|
}
|
|
|
|
if (messenger is IDelayedConfiguration simplMessenger)
|
|
{
|
|
simplMessenger.ConfigurationIsReady += Bridge_ConfigurationIsReady;
|
|
}
|
|
|
|
if (messenger is MobileControlBridgeBase roomBridge)
|
|
{
|
|
_roomBridges.Add(roomBridge);
|
|
}
|
|
|
|
this.LogVerbose(
|
|
"Adding messenger with key {messengerKey} for path {messengerPath}",
|
|
messenger.Key,
|
|
messenger.MessagePath
|
|
);
|
|
|
|
_messengers.Add(messenger.Key, messenger);
|
|
|
|
if (_initialized)
|
|
{
|
|
RegisterMessengerWithServer(messenger);
|
|
}
|
|
}
|
|
|
|
private void AddDefaultDeviceMessenger(IMobileControlMessenger messenger)
|
|
{
|
|
if (_defaultMessengers.ContainsKey(messenger.Key))
|
|
{
|
|
this.LogWarning(
|
|
"Default messenger with key {messengerKey} already added",
|
|
messenger.Key
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (messenger is IDelayedConfiguration simplMessenger)
|
|
{
|
|
simplMessenger.ConfigurationIsReady += Bridge_ConfigurationIsReady;
|
|
}
|
|
|
|
if (messenger is MobileControlBridgeBase roomBridge)
|
|
{
|
|
_roomBridges.Add(roomBridge);
|
|
}
|
|
|
|
this.LogVerbose(
|
|
"Adding default messenger with key {messengerKey} for path {messengerPath}",
|
|
messenger.Key,
|
|
messenger.MessagePath
|
|
);
|
|
|
|
_defaultMessengers.Add(messenger.Key, messenger);
|
|
|
|
if (_initialized)
|
|
{
|
|
RegisterMessengerWithServer(messenger);
|
|
}
|
|
}
|
|
|
|
private void RegisterMessengerWithServer(IMobileControlMessenger messenger)
|
|
{
|
|
this.LogVerbose(
|
|
"Registering messenger with key {messengerKey} for path {messengerPath}",
|
|
messenger.Key,
|
|
messenger.MessagePath
|
|
);
|
|
|
|
messenger.RegisterWithAppServer(this);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void Initialize()
|
|
{
|
|
foreach (var messenger in _messengers)
|
|
{
|
|
try
|
|
{
|
|
RegisterMessengerWithServer(messenger.Value);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.LogException(ex, "Exception registering custom messenger {messengerKey}", messenger.Key);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
foreach (var messenger in _defaultMessengers)
|
|
{
|
|
try
|
|
{
|
|
RegisterMessengerWithServer(messenger.Value);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.LogException(ex, "Exception registering default messenger {messengerKey}", messenger.Key);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
var simplMessengers = _messengers.Values.OfType<IDelayedConfiguration>().ToList();
|
|
|
|
if (simplMessengers.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_initialized = true;
|
|
|
|
RegisterSystemToServer();
|
|
}
|
|
|
|
#region IMobileControl Members
|
|
|
|
/// <summary>
|
|
/// Gets the App Server instance
|
|
/// </summary>
|
|
public static IMobileControl GetAppServer()
|
|
{
|
|
try
|
|
{
|
|
var appServer =
|
|
DeviceManager.GetDevices().SingleOrDefault(s => s is IMobileControl)
|
|
as MobileControlSystemController;
|
|
return appServer;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogMessage(e, "Unable to find MobileControlSystemController in Devices");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private bool CreateWebsocket()
|
|
{
|
|
_wsClient2?.Close();
|
|
_wsClient2 = null;
|
|
|
|
if (string.IsNullOrEmpty(SystemUuid))
|
|
{
|
|
this.LogError(
|
|
"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, message) => Utilities.ConvertWebsocketLog(data, message, this)
|
|
}
|
|
};
|
|
|
|
// setting to trace to let level be controlled by appdebug
|
|
_wsClient2.Log.Level = LogLevel.Trace;
|
|
|
|
_wsClient2.SslConfiguration.EnabledSslProtocols =
|
|
// System.Security.Authentication.SslProtocols.Tls11
|
|
System.Security.Authentication.SslProtocols.Tls12
|
|
| System.Security.Authentication.SslProtocols.Tls13;
|
|
|
|
_wsClient2.OnMessage += HandleMessage;
|
|
_wsClient2.OnOpen += HandleOpen;
|
|
_wsClient2.OnError += HandleError;
|
|
_wsClient2.OnClose += HandleClose;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Link the System Monitor to this App server
|
|
/// </summary>
|
|
public void LinkSystemMonitorToAppServer()
|
|
{
|
|
if (CrestronEnvironment.DevicePlatform != eDevicePlatform.Appliance)
|
|
{
|
|
this.LogWarning(
|
|
"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);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void SetWebsocketDebugLevel(string cmdparameters)
|
|
{
|
|
if (string.IsNullOrEmpty(cmdparameters))
|
|
{
|
|
this.LogInformation("Current Websocket debug level: {webSocketDebugLevel}", _wsLogLevel);
|
|
return;
|
|
}
|
|
|
|
if (cmdparameters.ToLower().Contains("help") || cmdparameters.ToLower().Contains("?"))
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
$"valid options are:\r\n{LogLevel.Trace}\r\n{LogLevel.Debug}\r\n{LogLevel.Info}\r\n{LogLevel.Warn}\r\n{LogLevel.Error}\r\n{LogLevel.Fatal}\r\n"
|
|
);
|
|
}
|
|
|
|
try
|
|
{
|
|
var debugLevel = (LogLevel)Enum.Parse(typeof(LogLevel), cmdparameters, true);
|
|
|
|
_wsLogLevel = debugLevel;
|
|
|
|
if (_wsClient2 != null)
|
|
{
|
|
_wsClient2.Log.Level = _wsLogLevel;
|
|
}
|
|
|
|
_directServer?.SetWebsocketLogLevel(_wsLogLevel);
|
|
|
|
CrestronConsole.ConsoleCommandResponse($"Websocket log level set to {debugLevel}");
|
|
}
|
|
catch
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
$"{cmdparameters} is not a valid debug level. Valid options are:\r\n{LogLevel.Trace}\r\n{LogLevel.Debug}\r\n{LogLevel.Info}\r\n{LogLevel.Warn}\r\n{LogLevel.Error}\r\n{LogLevel.Fatal}\r\n"
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
private void CrestronEnvironment_ProgramStatusEventHandler(
|
|
eProgramStatusEventType programEventType
|
|
)
|
|
{
|
|
if (
|
|
programEventType != eProgramStatusEventType.Stopping
|
|
|| _wsClient2 == null
|
|
|| !_wsClient2.IsAlive
|
|
)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_disableReconnect = true;
|
|
|
|
StopServerReconnectTimer();
|
|
CleanUpWebsocketClient();
|
|
}
|
|
|
|
/// <summary>
|
|
/// PrintActionDictionaryPaths method
|
|
/// </summary>
|
|
public void PrintActionDictionaryPaths(object o)
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse("ActionDictionary Contents:\r\n");
|
|
|
|
foreach (var (messengerKey, actionPath) in GetActionDictionaryPaths())
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse($"<{messengerKey}> {actionPath}\r\n");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get action paths for the current actions
|
|
/// </summary>
|
|
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<MessengerBase>();
|
|
foreach (var messenger in messengers)
|
|
{
|
|
foreach (var actionPath in messenger.GetActionPaths())
|
|
{
|
|
paths.Add((messenger.Key, $"{item.Key}{actionPath}"));
|
|
}
|
|
}
|
|
}
|
|
|
|
return paths;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an action to the dictionary
|
|
/// </summary>
|
|
/// <param name="messenger">The messenger for the API command</param>
|
|
/// <param name="action">The action to be triggered by the commmand</param>
|
|
public void AddAction<T>(T messenger, Action<string, string, JToken> action)
|
|
where T : IMobileControlMessenger
|
|
{
|
|
if (
|
|
_actionDictionary.TryGetValue(
|
|
messenger.MessagePath,
|
|
out List<IMobileControlAction> actionList
|
|
)
|
|
)
|
|
{
|
|
if (
|
|
actionList.Any(a =>
|
|
a.Messenger.GetType() == messenger.GetType()
|
|
&& a.Messenger.DeviceKey == messenger.DeviceKey
|
|
)
|
|
)
|
|
{
|
|
this.LogWarning("Messenger of type {messengerType} already exists. Skipping actions for {messengerKey}", messenger.GetType().Name, messenger.Key);
|
|
return;
|
|
}
|
|
|
|
actionList.Add(new MobileControlAction(messenger, action));
|
|
return;
|
|
}
|
|
|
|
actionList = new List<IMobileControlAction>
|
|
{
|
|
new MobileControlAction(messenger, action)
|
|
};
|
|
|
|
_actionDictionary.Add(messenger.MessagePath, actionList);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes an action from the dictionary
|
|
/// </summary>
|
|
/// <param name="key"></param>
|
|
/// <summary>
|
|
/// RemoveAction method
|
|
/// </summary>
|
|
public void RemoveAction(string key)
|
|
{
|
|
if (_actionDictionary.ContainsKey(key))
|
|
{
|
|
_actionDictionary.Remove(key);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the room bridge with the provided key
|
|
/// </summary>
|
|
/// <param name="key">The key of the room bridge</param>
|
|
public MobileControlBridgeBase GetRoomBridge(string key)
|
|
{
|
|
return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the room messenger with the provided key
|
|
/// </summary>
|
|
/// <param name="key">The Key of the rooom messenger</param>
|
|
public IMobileControlRoomMessenger GetRoomMessenger(string key)
|
|
{
|
|
return _roomBridges.FirstOrDefault((r) => r.RoomKey.Equals(key));
|
|
}
|
|
|
|
private void Bridge_ConfigurationIsReady(object sender, EventArgs e)
|
|
{
|
|
this.LogDebug("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)
|
|
{
|
|
this.LogDebug("Attempting to reconnect to server...");
|
|
|
|
ConnectWebsocketClient();
|
|
}
|
|
|
|
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)
|
|
{
|
|
this.LogDebug("System authorized, sending config.");
|
|
RegisterSystemToServer();
|
|
return;
|
|
}
|
|
|
|
this.LogInformation(response.Reason);
|
|
});
|
|
}
|
|
|
|
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 (Config.EnableApiServer)
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
"Mobile Control Edge Server API Information:\r\n\r\n" +
|
|
"\tServer address: {0}\r\n" +
|
|
"\tSystem Name: {1}\r\n" +
|
|
"\tSystem URL: {2}\r\n" +
|
|
"\tSystem UUID: {3}\r\n" +
|
|
"\tSystem User code: {4}\r\n" +
|
|
"\tConnected?: {5}\r\n" +
|
|
"\tSeconds Since Last Ack: {6}\r\n",
|
|
url,
|
|
name,
|
|
ConfigReader.ConfigObject.SystemUrl,
|
|
SystemUuid,
|
|
code,
|
|
conn,
|
|
secSinceLastAck.Seconds
|
|
);
|
|
}
|
|
else
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
"\r\nMobile Control Edge Server API Information:\r\n" +
|
|
" Not Enabled in Config.\r\n"
|
|
);
|
|
}
|
|
|
|
if (
|
|
Config.DirectServer != null
|
|
&& Config.DirectServer.EnableDirectServer
|
|
&& _directServer != null
|
|
)
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
"\r\nMobile Control Direct Server Information:\r\n" +
|
|
" User App URL: {0}\r\n" +
|
|
" Server port: {1}\r\n",
|
|
string.Format("{0}[insert_client_token]", _directServer.UserAppUrlPrefix),
|
|
_directServer.Port
|
|
);
|
|
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
"\r\n UI Client Info:\r\n" +
|
|
" Tokens Defined: {0}\r\n" +
|
|
" Clients Connected: {1}\r\n",
|
|
_directServer.UiClientContexts.Count,
|
|
_directServer.ConnectedUiClientsCount
|
|
);
|
|
|
|
var clientNo = 1;
|
|
foreach (var clientContext in _directServer.UiClientContexts)
|
|
{
|
|
var clients = _directServer.UiClients.Values.Where(c => c.TokenKey == clientContext.Key);
|
|
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
$"\r\nClient {clientNo}:\r\n" +
|
|
$" Room Key: {clientContext.Value.Token.RoomKey}\r\n" +
|
|
$" Touchpanel Key: {clientContext.Value.Token.TouchpanelKey}\r\n" +
|
|
$" Token: {clientContext.Key}\r\n" +
|
|
$" Client URL: {_directServer.UserAppUrlPrefix}{clientContext.Key}\r\n" +
|
|
$" Clients:\r\n"
|
|
);
|
|
|
|
if (!clients.Any())
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse(" No clients connected");
|
|
}
|
|
foreach (var client in clients)
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
$" ID: {client.Id}\r\n" +
|
|
$" Connected: {client.Context.WebSocket.IsAlive}\r\n" +
|
|
$" Duration: {(client.Context.WebSocket.IsAlive ? client.ConnectedDuration.TotalSeconds.ToString() : "Not Connected")}\r\n"
|
|
);
|
|
}
|
|
|
|
clientNo++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
"\r\nMobile Control Direct Server Information:\r\n" +
|
|
" Not Enabled in Config.\r\n"
|
|
);
|
|
}
|
|
|
|
var expectedAppVersion = ConfigReader.ConfigObject?.Versions?.TouchpanelWrapperApp?.Version;
|
|
var userAppPath = Global.FilePathPrefix + "mcUserApp" + Global.DirectorySeparator;
|
|
var versionCheck = TouchpanelWrapperAppVersionChecker.CheckDeployedVersion(userAppPath, expectedAppVersion);
|
|
|
|
CrestronConsole.ConsoleCommandResponse(
|
|
$"\r\nUI Wrapper App Deployed Version Check:\r\n {versionCheck.Summary}\r\n"
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register this system to the Mobile Control Edge Server
|
|
/// </summary>
|
|
public void RegisterSystemToServer()
|
|
{
|
|
|
|
if (!Config.EnableApiServer)
|
|
{
|
|
this.LogInformation(
|
|
"ApiServer disabled via config. Cancelling attempt to register to server."
|
|
);
|
|
return;
|
|
}
|
|
|
|
var result = CreateWebsocket();
|
|
|
|
if (!result)
|
|
{
|
|
this.LogFatal("Unable to create websocket.");
|
|
return;
|
|
}
|
|
|
|
ConnectWebsocketClient();
|
|
}
|
|
|
|
private void ConnectWebsocketClient()
|
|
{
|
|
lock (_wsCriticalSection)
|
|
{
|
|
// 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;
|
|
|
|
this.LogDebug(
|
|
"Connecting mobile control client to {mobileControlUrl}",
|
|
_wsClient2.Url
|
|
);
|
|
|
|
TryConnect();
|
|
}
|
|
}
|
|
|
|
private void TryConnect()
|
|
{
|
|
try
|
|
{
|
|
IsAuthorized = false;
|
|
_wsClient2.Connect();
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
this.LogError(
|
|
"Maximum retries exceeded. Restarting websocket"
|
|
);
|
|
HandleConnectFailure();
|
|
}
|
|
catch (IOException ex)
|
|
{
|
|
this.LogException(ex, "IO Exception on connect");
|
|
HandleConnectFailure();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.LogException(
|
|
ex,
|
|
"Error on Websocket Connect"
|
|
);
|
|
HandleConnectFailure();
|
|
}
|
|
}
|
|
|
|
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) =>
|
|
this.LogDebug(
|
|
"Message from websocket: {message}",
|
|
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();
|
|
this.LogInformation("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)
|
|
{
|
|
this.LogError("Websocket error {0}", e.Message);
|
|
|
|
IsAuthorized = false;
|
|
StartServerReconnectTimer();
|
|
}
|
|
|
|
private void HandleClose(object sender, CloseEventArgs e)
|
|
{
|
|
this.LogDebug(
|
|
"Websocket close {code} {reason}, clean={wasClean}",
|
|
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();
|
|
}
|
|
|
|
private void SendInitialMessage()
|
|
{
|
|
this.LogInformation("Sending initial join message");
|
|
|
|
var touchPanels = DeviceManager
|
|
.AllDevices.OfType<IMobileControlTouchpanelController>()
|
|
.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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the Essentials configuration with version data
|
|
/// </summary>
|
|
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;
|
|
|
|
|
|
// // Set for local testing
|
|
// confObject.RuntimeInfo.PluginVersion = "4.0.0-localBuild";
|
|
|
|
// Populate the plugin version
|
|
var pluginVersion = Assembly
|
|
.GetExecutingAssembly()
|
|
.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
|
|
|
|
|
|
if (pluginVersion[0] is AssemblyInformationalVersionAttribute fullVersionAtt)
|
|
{
|
|
var pluginInformationalVersion = fullVersionAtt.InformationalVersion;
|
|
|
|
confObject.RuntimeInfo.PluginVersion = pluginInformationalVersion;
|
|
confObject.RuntimeInfo.EssentialsVersion = Global.AssemblyVersion;
|
|
confObject.RuntimeInfo.PepperDashCoreVersion = PluginLoader.PepperDashCoreAssembly.Version;
|
|
confObject.RuntimeInfo.EssentialsPlugins = PluginLoader.EssentialsPluginAssemblies;
|
|
}
|
|
return confObject;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the Client URL for a given room
|
|
/// </summary>
|
|
/// <param name="path">new App URL</param>
|
|
/// <param name="roomKey">room key. Default is null</param>
|
|
/// <remarks>
|
|
/// If roomKey is null, the URL will be set for the entire system.
|
|
/// </remarks>
|
|
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<string> { Value = path })
|
|
};
|
|
|
|
SendMessageObject(message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sends any object type to server
|
|
/// </summary>
|
|
/// <param name="o"></param>
|
|
public void SendMessageObject(IMobileControlMessage o)
|
|
{
|
|
|
|
if (Config.EnableApiServer)
|
|
{
|
|
|
|
_transmitToServerQueue.Enqueue(new TransmitMessage(o, _wsClient2));
|
|
|
|
}
|
|
|
|
if (
|
|
Config.DirectServer != null
|
|
&& Config.DirectServer.EnableDirectServer
|
|
&& _directServer != null
|
|
)
|
|
{
|
|
_transmitToClientsQueue.Enqueue(new MessageToClients(o, _directServer));
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Send a message to a client using the Direct Server
|
|
/// </summary>
|
|
/// <param name="o">object to send</param>
|
|
public void SendMessageObjectToDirectClient(object o)
|
|
{
|
|
if (
|
|
Config.DirectServer != null
|
|
&& Config.DirectServer.EnableDirectServer
|
|
&& _directServer != null
|
|
)
|
|
{
|
|
_transmitToClientsQueue.Enqueue(new MessageToClients(o, _directServer));
|
|
}
|
|
}
|
|
|
|
private void CleanUpWebsocketClient()
|
|
{
|
|
if (_wsClient2 == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
this.LogDebug("Disconnecting websocket");
|
|
|
|
_wsClient2.Close();
|
|
}
|
|
|
|
private void ResetPingTimer()
|
|
{
|
|
// This tells us we're online with the API and getting pings
|
|
_pingTimer.Stop();
|
|
_pingTimer.Interval = PingInterval;
|
|
_pingTimer.Start();
|
|
}
|
|
|
|
private void StartPingTimer()
|
|
{
|
|
StopPingTimer();
|
|
_pingTimer = new Timer(PingInterval) { AutoReset = false };
|
|
_pingTimer.Elapsed += (s, e) => PingTimerCallback(null);
|
|
_pingTimer.Start();
|
|
}
|
|
|
|
private void StopPingTimer()
|
|
{
|
|
if (_pingTimer == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_pingTimer.Stop();
|
|
_pingTimer.Dispose();
|
|
_pingTimer = null;
|
|
}
|
|
|
|
private void PingTimerCallback(object o)
|
|
{
|
|
this.LogDebug(
|
|
|
|
"Ping timer expired. Closing websocket"
|
|
);
|
|
|
|
try
|
|
{
|
|
_wsClient2.Close();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.LogException(ex,
|
|
"Exception closing websocket"
|
|
);
|
|
|
|
HandleConnectFailure();
|
|
}
|
|
}
|
|
|
|
private void StartServerReconnectTimer()
|
|
{
|
|
StopServerReconnectTimer();
|
|
_serverReconnectTimer = new Timer(ServerReconnectInterval) { AutoReset = false };
|
|
_serverReconnectTimer.Elapsed += (s, e) => ReconnectToServerTimerCallback(null);
|
|
_serverReconnectTimer.Start();
|
|
this.LogDebug("Reconnect Timer Started.");
|
|
}
|
|
|
|
private void StopServerReconnectTimer()
|
|
{
|
|
if (_serverReconnectTimer == null)
|
|
{
|
|
return;
|
|
}
|
|
_serverReconnectTimer.Stop();
|
|
_serverReconnectTimer = null;
|
|
}
|
|
|
|
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<string>());
|
|
}
|
|
}
|
|
|
|
private void HandleClientJoined(JToken content)
|
|
{
|
|
var clientId = content["clientId"].Value<string>();
|
|
var roomKey = content["roomKey"].Value<string>();
|
|
var touchpanelKey = content.SelectToken("touchpanelKey");
|
|
|
|
if (_roomCombiner == null)
|
|
{
|
|
var message = new MobileControlMessage
|
|
{
|
|
Type = "/system/roomKey",
|
|
ClientId = clientId,
|
|
Content = roomKey
|
|
};
|
|
|
|
SendMessageObject(message);
|
|
|
|
SendDeviceInterfaces(clientId);
|
|
|
|
SendTouchpanelKey(clientId, touchpanelKey);
|
|
return;
|
|
}
|
|
|
|
if (_roomCombiner.CurrentScenario == null)
|
|
{
|
|
var message = new MobileControlMessage
|
|
{
|
|
Type = "/system/roomKey",
|
|
ClientId = clientId,
|
|
Content = roomKey
|
|
};
|
|
|
|
SendMessageObject(message);
|
|
|
|
SendDeviceInterfaces(clientId);
|
|
|
|
SendTouchpanelKey(clientId, touchpanelKey);
|
|
return;
|
|
}
|
|
|
|
if (!_roomCombiner.CurrentScenario.UiMap.ContainsKey(roomKey))
|
|
{
|
|
|
|
this.LogWarning(
|
|
"Unable to find correct roomKey for {roomKey} in current scenario. Returning {roomKey} as roomKey", roomKey);
|
|
|
|
var message = new MobileControlMessage
|
|
{
|
|
Type = "/system/roomKey",
|
|
ClientId = clientId,
|
|
Content = roomKey
|
|
};
|
|
|
|
SendMessageObject(message);
|
|
|
|
SendDeviceInterfaces(clientId);
|
|
|
|
SendTouchpanelKey(clientId, touchpanelKey);
|
|
return;
|
|
}
|
|
|
|
var newRoomKey = _roomCombiner.CurrentScenario.UiMap[roomKey];
|
|
|
|
var newMessage = new MobileControlMessage
|
|
{
|
|
Type = "/system/roomKey",
|
|
ClientId = clientId,
|
|
Content = newRoomKey
|
|
};
|
|
|
|
SendMessageObject(newMessage);
|
|
|
|
SendDeviceInterfaces(clientId);
|
|
|
|
SendTouchpanelKey(clientId, touchpanelKey);
|
|
}
|
|
|
|
private void SendTouchpanelKey(string clientId, JToken touchpanelKeyToken)
|
|
{
|
|
if (touchpanelKeyToken == null)
|
|
{
|
|
this.LogWarning("Touchpanel key not found for client {clientId}", clientId);
|
|
return;
|
|
}
|
|
|
|
SendMessageObject(new MobileControlMessage
|
|
{
|
|
Type = "/system/touchpanelKey",
|
|
ClientId = clientId,
|
|
Content = touchpanelKeyToken.Value<string>()
|
|
});
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles a batch request for device status. Supports two content formats:
|
|
/// <para>
|
|
/// Granular format: <c>{ "devices": { "deviceKey": ["/fullStatus", "/layoutStatus"], ... } }</c>
|
|
/// </para>
|
|
/// <para>
|
|
/// Legacy format: <c>{ "deviceKeys": ["deviceKey1", "deviceKey2"] }</c> (equivalent to /fullStatus for each)
|
|
/// </para>
|
|
/// Triggers the specified action paths for each device in parallel,
|
|
/// then sends an /system/initialSyncComplete message after all have been processed.
|
|
/// </summary>
|
|
private void HandleBatchDeviceFullStatus(string clientId, JToken content)
|
|
{
|
|
if (content == null)
|
|
{
|
|
this.LogWarning("BatchDeviceFullStatus: content is null");
|
|
return;
|
|
}
|
|
|
|
// Build a dictionary of deviceKey -> list of action paths
|
|
Dictionary<string, List<string>> deviceActionPaths;
|
|
|
|
var devicesToken = content.SelectToken("devices");
|
|
if (devicesToken != null)
|
|
{
|
|
// Granular format: { "devices": { "key": ["/path1", "/path2"], ... } }
|
|
deviceActionPaths = devicesToken.ToObject<Dictionary<string, List<string>>>();
|
|
}
|
|
else
|
|
{
|
|
// Legacy format: { "deviceKeys": ["key1", "key2"] } -> each gets /fullStatus
|
|
var deviceKeys = content.SelectToken("deviceKeys")?.ToObject<List<string>>();
|
|
|
|
if (deviceKeys == null || deviceKeys.Count == 0)
|
|
{
|
|
this.LogWarning("BatchDeviceFullStatus: No device keys or devices provided");
|
|
SendMessageObject(new MobileControlMessage
|
|
{
|
|
Type = "/system/initialSyncComplete",
|
|
ClientId = clientId
|
|
});
|
|
return;
|
|
}
|
|
|
|
deviceActionPaths = deviceKeys.ToDictionary(k => k, _ => new List<string> { "/fullStatus" });
|
|
}
|
|
|
|
if (deviceActionPaths == null || deviceActionPaths.Count == 0)
|
|
{
|
|
this.LogWarning("BatchDeviceFullStatus: Empty devices dictionary");
|
|
SendMessageObject(new MobileControlMessage
|
|
{
|
|
Type = "/system/initialSyncComplete",
|
|
ClientId = clientId
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.LogInformation("BatchDeviceFullStatus: Processing {count} devices", deviceActionPaths.Count);
|
|
|
|
var tasks = new List<Task>();
|
|
|
|
foreach (var kvp in deviceActionPaths)
|
|
{
|
|
var deviceKey = kvp.Key;
|
|
var actionPaths = kvp.Value;
|
|
|
|
if (actionPaths == null || actionPaths.Count == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (var actionPath in actionPaths)
|
|
{
|
|
var fullPath = $"/device/{deviceKey}{actionPath}";
|
|
|
|
var handlers = _actionDictionary
|
|
.Where(kv => fullPath.StartsWith(kv.Key + "/"))
|
|
.SelectMany(kv => kv.Value)
|
|
.ToList();
|
|
|
|
if (handlers.Count == 0)
|
|
{
|
|
this.LogDebug("BatchDeviceFullStatus: No handlers for {deviceKey} at path {actionPath}", deviceKey, actionPath);
|
|
continue;
|
|
}
|
|
|
|
foreach (var handler in handlers)
|
|
{
|
|
tasks.Add(Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
handler.Action(fullPath, clientId, JToken.FromObject(new { deviceKey }));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.LogError("BatchDeviceFullStatus: Exception in handler for {deviceKey} at {actionPath}: {message}", deviceKey, actionPath, ex.Message);
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
// After all handlers have completed and enqueued their responses, send sync complete
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await Task.WhenAll(tasks);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.LogError("BatchDeviceFullStatus: Exception waiting for tasks: {message}", ex.Message);
|
|
}
|
|
|
|
SendMessageObject(new MobileControlMessage
|
|
{
|
|
Type = "/system/initialSyncComplete",
|
|
ClientId = clientId
|
|
});
|
|
});
|
|
}
|
|
|
|
private void SendDeviceInterfaces(string clientId)
|
|
{
|
|
this.LogDebug("Sending Device interfaces");
|
|
var devices = DeviceManager.GetDevices();
|
|
Dictionary<string, DeviceInterfaceInfo> deviceInterfaces = new Dictionary<string, DeviceInterfaceInfo>();
|
|
|
|
foreach (var device in devices)
|
|
{
|
|
var interfaces = device?.GetType().GetInterfaces().Select((i) => i.Name).ToList() ?? new List<string>();
|
|
|
|
deviceInterfaces.Add(device.Key, new DeviceInterfaceInfo
|
|
{
|
|
Key = device.Key,
|
|
Name = (device as IKeyName)?.Name ?? "",
|
|
Interfaces = interfaces
|
|
});
|
|
}
|
|
|
|
var message = new MobileControlMessage
|
|
{
|
|
Type = "/system/deviceInterfaces",
|
|
ClientId = clientId,
|
|
Content = JToken.FromObject(new { deviceInterfaces })
|
|
};
|
|
|
|
SendMessageObject(message);
|
|
}
|
|
|
|
private void HandleUserCode(JToken content, Action<string, string> action = null)
|
|
{
|
|
var code = content["userCode"];
|
|
|
|
JToken qrChecksum;
|
|
|
|
try
|
|
{
|
|
qrChecksum = content.SelectToken("qrChecksum", false);
|
|
}
|
|
catch
|
|
{
|
|
qrChecksum = new JValue(string.Empty);
|
|
}
|
|
|
|
if (code == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (action == null)
|
|
{
|
|
foreach (var bridge in _roomBridges)
|
|
{
|
|
bridge.SetUserCode(code.Value<string>(), qrChecksum.Value<string>());
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
action(code.Value<string>(), qrChecksum.Value<string>());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enqueue an incoming message for processing
|
|
/// </summary>
|
|
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"))
|
|
{
|
|
this.LogDebug(
|
|
"Message RX: {messageText}",
|
|
messageText
|
|
);
|
|
}
|
|
|
|
try
|
|
{
|
|
var message = JsonConvert.DeserializeObject<MobileControlMessage>(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/batchDeviceFullStatus":
|
|
HandleBatchDeviceFullStatus(message.ClientId, message.Content);
|
|
break;
|
|
case "/system/reboot":
|
|
SystemMonitorController.ProcessorReboot();
|
|
break;
|
|
case "/system/programReset":
|
|
SystemMonitorController.ProgramReset(InitialParametersClass.ApplicationNumber);
|
|
break;
|
|
case "raw":
|
|
var wrapper = message.Content.ToObject<DeviceActionWrapper>();
|
|
DeviceJsonApi.DoDeviceAction(wrapper);
|
|
break;
|
|
case "close":
|
|
this.LogDebug("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 handlers = _actionDictionary.Where(kv => message.Type.StartsWith(kv.Key + "/")).SelectMany(kv => kv.Value).ToList(); // adds trailing slash to ensure above case is handled
|
|
|
|
|
|
if (handlers.Count == 0)
|
|
{
|
|
this.LogInformation("-- Warning: Incoming message has no registered handler {type}", message.Type);
|
|
break;
|
|
}
|
|
|
|
foreach (var handler in handlers)
|
|
{
|
|
Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
handler.Action(message.Type, message.ClientId, message.Content);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.LogError(
|
|
"Exception in handler for message type {type}, ClientId {clientId}",
|
|
message.Type,
|
|
message.ClientId
|
|
);
|
|
this.LogDebug(ex, "Stack Trace: ");
|
|
}
|
|
}).ContinueWith(task =>
|
|
{
|
|
if (task.IsFaulted && task.Exception != null)
|
|
{
|
|
this.LogError(
|
|
"Unhandled exception in Task for message type {type}, ClientId {clientId}",
|
|
message.Type,
|
|
message.ClientId
|
|
);
|
|
this.LogDebug(task.Exception.GetBaseException(), "Stack Trace: ");
|
|
}
|
|
}, TaskContinuationOptions.OnlyOnFaulted);
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
catch (Exception err)
|
|
{
|
|
this.LogException(
|
|
err,
|
|
"Unable to parse {message}",
|
|
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");
|
|
}
|
|
}
|
|
} |