using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using ETSI.ARF.OpenAPI.WorldAnalysis; using ETSI.ARF.WorldAnalysis; using static WorldAnalysisInterface; using ETSI.ARF.WorldAnalysis.REST; using WebSocketSharp; //Implementation of the WorldAnalysis interface public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface { // // Inspector variables // /// <summary> /// WorldSAnalysisServer /// </summary> public WorldAnalysisServer waServer; public string token = "ETSI-ARF-STF"; public string sessionID = "RESTful-API"; [Space(8)] public bool isDebug = false; // // Private members // private WorldAnalysisClient apiClient; // For sync calls private WorldAnalysisClient apiClientAsync; // For async calls private WebSocketSharp.WebSocket webSocket; // For WebSockets private bool websocketConnected = false; // // Management of subscriptions // /// <summary> /// Dictionnary of susbscription informations for poses, for each item, stored using the UUID of the item (anchor/trackable) /// </summary> private Dictionary<Guid, SubscriptionInfo> m_subscriptionsPoses; public struct SubscriptionInfo { public Guid uuid; // id of subscription (id is defined by the WA server) public Guid uuidTarget; // id trackable or anchor public float timeValidity; //The duration of the validity of the subscription public ETSI.ARF.OpenAPI.WorldAnalysis.Pose pose; public PoseCallback callback; } #region Unity_Methods /// <summary> /// Unity Awake Method /// </summary> protected void Awake() { Instance = this; //m_relocalizationInformations = new Dictionary<Guid, ETSI.ARF.OpenAPI.WorldStorage.RelocalizationInformation>(); //m_computedPoses = new Dictionary<Guid, ETSI.ARF.OpenAPI.WorldAnalysis.Pose>(); m_subscriptionsPoses = new Dictionary<Guid, SubscriptionInfo>(); // sync var httpClient = new BasicHTTPClient(waServer.URI); apiClient = new WorldAnalysisClient(httpClient); // async //var httpClientAsync = new UnityWebRequestHttpClient(waServer.URI); //apiClientAsync = new WorldAnalysisClient(httpClientAsync); } /// <summary> /// Unity Start Method /// </summary> protected void Start() { } /// <summary> /// Unity Update Method /// </summary> protected void Update() { ManageSubscriptionValidity(); // todo: Call subscription callback(s) here or in the websocket?!? foreach (KeyValuePair<Guid, SubscriptionInfo> subPose in m_subscriptionsPoses) { } } #endregion #region Test methods public void CheckServer() { string ping = AdminRequest.PingSync(waServer); string state = AdminRequest.AdminSync(waServer); string ver = AdminRequest.VersionSync(waServer); Debug.Log("[REST] WA Ping: " + ping); Debug.Log("[REST] WA State: " + state); Debug.Log("[REST] WA Version: " + ver); } public void PrintCapabilities(Capability[] capabilities) { string res = ""; Debug.Log("[REST] Got " + capabilities.Length + " capabilities."); foreach (var item in capabilities) { res += "\nCapability: " + item.TrackableType + " Version: " + item.EncodingInformation.Version; } res += "\nEnd of capabilities."; Debug.Log("[REST] Capabilities: " + res); } #endregion #region Communication system private void CreateWebHookServer() { throw new Exception("[REST] CreateWebHookServer(): Not implemented!"); } private void DestroyWebHookServer() { return; } public WebSocket OpenWebSocketClient(string url) { webSocket = new WebSocketSharp.WebSocket(url); // // Define standard callbacks // webSocket.OnOpen += (sender, e) => { Debug.Log("[WS] Connected"); websocketConnected = true; webSocket.Send("RegisterClient:UnitySceneManagement"); }; webSocket.OnClose += (sender, e) => { Debug.Log("[WS] Disconnected"); websocketConnected = false; }; webSocket.OnError += (sender, e) => Debug.Log("[WS] Error!"); webSocket.OnMessage += (sender, e) => HandleWebSocketClient(e.Data); webSocket.Connect(); return webSocket; } private void OnDestroy() { // State: red CloseWebSocketClient(); } private void CloseWebSocketClient() { if (websocketConnected) { webSocket.Send("UnregisterClient"); webSocket.Close(); } } bool ok = false; public void HandleWebSocketClient(string data) { Debug.Log("[WS] Receiving: " + data); if (data.Contains("You are now registered")) { ok = true; if (isDebug) webSocket.Send("PoseStart:10"); // test } else if (data == "PoseStop") { //SetColor(Color.yellow); } else if (ok) { if (data.Contains("estimationState")) { // Handle pose ETSI.ARF.OpenAPI.WorldAnalysis.Pose p = JsonUtility.FromJson<ETSI.ARF.OpenAPI.WorldAnalysis.Pose>(data); Debug.Log("[WS][Pose] State: " + p.EstimationState.ToString()); PoseEstimationResult res = p.EstimationState == PoseEstimationState.OK ? PoseEstimationResult.OK : PoseEstimationResult.FAILURE; // Search the corresponding callbacks foreach (var item in m_subscriptionsPoses.Values) { if (p.Uuid == item.uuidTarget) { item.callback(res, p); } } } } } #endregion #region Lifecycle /// <summary> /// Check the validity of all subscriptions and delete one if needed /// </summary> protected void ManageSubscriptionValidity() { if (m_subscriptionsPoses.Count == 0) return; List<Guid> subscriptionToDelete = new List<Guid>(); foreach (KeyValuePair<Guid, SubscriptionInfo> sub in m_subscriptionsPoses) { float validity = sub.Value.timeValidity; if (Time.time > validity) { subscriptionToDelete.Add(sub.Key); } } foreach (Guid s in subscriptionToDelete) { Debug.Log("ETSI ARF : Subscription deleted " + s); m_subscriptionsPoses.Remove(s); } } #endregion #region ARF_API // // Implementation of the endpoints // public AskFrameRateResult SetPoseEstimationFramerate(string token, PoseConfigurationTrackableType type, EncodingInformationStructure encodingInformation, int minimumFramerate) { PoseConfiguration poseConfig = new PoseConfiguration(); poseConfig.TrackableType = type; poseConfig.EncodingInformation = encodingInformation; poseConfig.Framerate = minimumFramerate; apiClient.ConfigureFramerate(token, sessionID, poseConfig); return AskFrameRateResult.OK; } public PoseEstimationResult GetLastPose(string token, Guid uuid, Mode_WorldAnalysis mode, out ETSI.ARF.OpenAPI.WorldAnalysis.Pose pose) { pose = apiClient.GetPose(token, sessionID, uuid, mode); return pose != null ? PoseEstimationResult.OK : PoseEstimationResult.NOT_SUPPORTED; } public PoseEstimationResult[] GetLastPoses(string token, Guid[] uuids, Mode_WorldAnalysis[] modes, out ETSI.ARF.OpenAPI.WorldAnalysis.Pose[] poses) { if (uuids.Length != modes.Length) { Debug.LogError("[REST] ETSI ARF: Get poses: uuids and modes array do no have the same length"); poses = null; return null; } PoseEstimationResult[] resul = new PoseEstimationResult[uuids.Length]; poses = new ETSI.ARF.OpenAPI.WorldAnalysis.Pose[uuids.Length]; List<Anonymous> uuidList = new List<Anonymous>(); Response poses_ = apiClient.GetPoses(token, sessionID, uuidList.ToArray()); List<ETSI.ARF.OpenAPI.WorldAnalysis.Pose> posesList = poses_.Poses as List<ETSI.ARF.OpenAPI.WorldAnalysis.Pose>; if (poses_ != null && posesList != null && posesList.Count > 0) { for (int i = 0; i < uuids.Length; i++) { PoseEstimationResult poseResul = new PoseEstimationResult(); resul[i] = poseResul; poses[i] = posesList[i]; } return resul; } else { poses = null; return null; } } public InformationSubscriptionResult SubscribeToPose(string token, Guid uuid, Mode_WorldAnalysis mode, PoseCallback callback, ref int validity, out Guid subscriptionUUID) { // Todo: Maintain the callback to the subscription id // Get capabilities? // Get reloc info? SubscriptionSingleRequest body = new SubscriptionSingleRequest(); body.Target = uuid; body.Mode = mode; body.Validity = validity; body.WebhookUrl = callback == null ? "" : "https:\\..."; // empty -> app will use websockets (client)! // Get subscription info from the REST server SubscriptionSingle response = apiClient.SubscribeToPose(token, sessionID, body); subscriptionUUID = response.Uuid; // We add the subscription SubscriptionInfo sub = new SubscriptionInfo(); sub.uuid = response.Uuid; sub.timeValidity = Time.time + (validity / 1000.0f); sub.pose = new ETSI.ARF.OpenAPI.WorldAnalysis.Pose(); sub.pose.Mode = mode; sub.uuidTarget = uuid; sub.callback = callback; m_subscriptionsPoses.Add(sub.uuid, sub); if (!string.IsNullOrEmpty(response.WebhookUrl)) { CloseWebSocketClient(); // todo: create a REST server so that the WA server can send pose update to it // How to auto-generate the C# REST server for pose for Unity? CreateWebHookServer(); } else { DestroyWebHookServer(); // todo: Open the websocket? string websocketUrl = response.WebsocketUrl; if (isDebug) websocketUrl = "ws://localhost:61788/ws"; // for tests if (string.IsNullOrEmpty(websocketUrl)) { // Create the WebSockets client here (NOT in the scene scripts) if (!websocketConnected) OpenWebSocketClient(websocketUrl); } else throw new Exception("[REST] No valid WebSockets URL in server reponse."); } return InformationSubscriptionResult.OK; } public InformationSubscriptionResult[] SubscribeToPoses(string token, Guid[] uuids, Mode_WorldAnalysis[] modes, PoseCallback callback, ref int validity, out Guid[] subscriptionUUIDs) { if (uuids.Length != 0 && uuids.Length == modes.Length) { InformationSubscriptionResult[] resul = new InformationSubscriptionResult[uuids.Length]; subscriptionUUIDs = new Guid[uuids.Length]; for (int i = 0; i < uuids.Length; i++) { resul[i] = SubscribeToPose(token, uuids[i], modes[i], callback, ref validity, out subscriptionUUIDs[i]); } return resul; } else { Debug.LogError("[REST] ETSI ARF: Subscribe poses: uuids and modes array do no have the same length"); subscriptionUUIDs = null; return null; } } public InformationSubscriptionResult GetSubsription(string token, Guid subscriptionUUID, out PoseCallback callback, out Guid target, out Mode_WorldAnalysis mode, out int validity) { // default callback = null; target = Guid.Empty; mode = Mode_WorldAnalysis.TRACKABLES_TO_DEVICE; validity = 0; if (m_subscriptionsPoses.ContainsKey(subscriptionUUID)) { // Check the server subscription SubscriptionSingle sub = apiClient.GetSubscription(token, sessionID, subscriptionUUID); // Check local one if (sub.Uuid == subscriptionUUID) { SubscriptionInfo subInfo = m_subscriptionsPoses[subscriptionUUID]; callback = subInfo.callback; target = subInfo.uuidTarget; mode = subInfo.pose.Mode; float validitySeconds = subInfo.timeValidity - Time.time; validity = (int)validitySeconds * 1000;// conversion in ms // Compare both if (target == sub.Target && mode == sub.Mode && validity == sub.Validity) return InformationSubscriptionResult.OK; else return InformationSubscriptionResult.NOT_ALLOWED; } } return InformationSubscriptionResult.UNKNOWN_ID; } public InformationSubscriptionResult UpdateSubscription(string token, Guid subscriptionUUID, Mode_WorldAnalysis mode, int validity, PoseCallback callback) { // default callback = null; mode = Mode_WorldAnalysis.TRACKABLES_TO_DEVICE; validity = 0; if (m_subscriptionsPoses.ContainsKey(subscriptionUUID)) { SubscriptionInfo sub = m_subscriptionsPoses[subscriptionUUID]; PoseCallback oldCB = sub.callback; Body body = new Body(); body.Mode = mode; body.Validity = validity; body.WebhookUrl = callback == null ? "" : "https:\\..."; // empty -> app will use websockets (client)! // Update subscription info in the REST server SubscriptionSingle response = apiClient.UpdateSubscription(token, sessionID, subscriptionUUID, body); // Update local data sub.pose.Mode = response.Mode; sub.callback = callback; sub.timeValidity = Time.time + (validity / 1000.0f); // // Recreate server/connection to ws only if someone changed! // if (oldCB != null && callback == null && !string.IsNullOrEmpty(response.WebhookUrl)) { CloseWebSocketClient(); // todo: create a REST server so that the WA server can send pose update to it // How to auto-generate the C# REST server for pose for Unity? CreateWebHookServer(); } else if (oldCB == null && callback != null && string.IsNullOrEmpty(response.WebhookUrl)) { DestroyWebHookServer(); // todo: Open the websocket? string websocketUrl = response.WebsocketUrl; if (isDebug) websocketUrl = "ws://localhost:61788/ws"; // for tests if (string.IsNullOrEmpty(websocketUrl)) { // Create the WebSockets client here (NOT in the scene scripts) if (!websocketConnected) OpenWebSocketClient(websocketUrl); } else throw new Exception("[REST] No valid WebSockets URL in server reponse."); } return InformationSubscriptionResult.OK; } return InformationSubscriptionResult.UNKNOWN_ID; } public InformationSubscriptionResult UnSubscribeToPose(Guid subscriptionUUID) { if (m_subscriptionsPoses.ContainsKey(subscriptionUUID)) { apiClient.UnsubscribeFromPose(token, sessionID, subscriptionUUID); m_subscriptionsPoses.Remove(subscriptionUUID); if (m_subscriptionsPoses.Count == 0) { // Close the connection via websockets CloseWebSocketClient(); } return InformationSubscriptionResult.OK; } return InformationSubscriptionResult.UNKNOWN_ID; } public CapabilityResult GetCapabilities(string token, out Capability[] capabilities) { Response2 cap = apiClient.GetCapabilities(token, sessionID); if (cap == null || cap.Capabilities == null || cap.Capabilities.Count == 0) { capabilities = null; return CapabilityResult.FAIL; } else { capabilities = new Capability[cap.Capabilities.Count]; cap.Capabilities.CopyTo(capabilities, 0); return CapabilityResult.OK; } } public CapabilityResult GetCapability(string token, Guid uuid, out bool isSupported, out TypeWorldStorage type, out Capability[] capability) { isSupported = false; type = TypeWorldStorage.UNKNOWN; capability = null; Response3 cap = apiClient.GetSupport(token, sessionID, uuid); if (cap == null || cap.Capabilities == null || cap.Capabilities.Count == 0) { isSupported = false; capability = null; return CapabilityResult.FAIL; } else { isSupported = true; capability = new Capability[cap.Capabilities.Count]; cap.Capabilities.CopyTo(capability, 0); return CapabilityResult.OK; } } #endregion }