diff --git a/Runtime/Scripts/WorldAnalysisREST.cs b/Runtime/Scripts/WorldAnalysisREST.cs index ca881ccf6ac770dcc857c323484b98e8d2e0249b..439871247d7de73707a3a813881c61749c5c6526 100644 --- a/Runtime/Scripts/WorldAnalysisREST.cs +++ b/Runtime/Scripts/WorldAnalysisREST.cs @@ -1,23 +1,54 @@ 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 { - static protected string token = "ARF_Permission"; - + // + // Inspector variables + // + /// <summary> + /// WorldSAnalysisServer + /// </summary> public WorldAnalysisServer waServer; + public string token = "ETSI-ARF-STF"; + public string sessionID = "RESTful-API"; - // For sync calls - private WorldAnalysisClient apiClient; + [Space(8)] + public bool isDebug = false; - // For async calls - private WorldAnalysisClient apiClientAsync; + // + // 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 @@ -26,6 +57,11 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface /// </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); @@ -47,6 +83,12 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface /// </summary> protected void Update() { + ManageSubscriptionValidity(); + + // todo: Call subscription callback(s) here or in the websocket?!? + foreach (KeyValuePair<Guid, SubscriptionInfo> subPose in m_subscriptionsPoses) + { + } } #endregion @@ -62,29 +104,130 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface Debug.Log("[REST] WA Version: " + ver); } - public string GetWebSocketEndpoint() + 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) { - string res = "empty"; + 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(); - SubscriptionSingleRequest param = new SubscriptionSingleRequest(); - param.Mode = Mode_WorldAnalysis.DEVICE_TO_TRACKABLES; - param.Target = Guid.Parse("fa8bbe40-8052-11ec-a8a3-0242ac120002"); // test + return webSocket; + } - SubscriptionSingle response = apiClient.SubscribeToPose(token, "1", param); - res = response.WebsocketUrl; - return res; - } - - public void PrintCapabilities() + private void OnDestroy() { - string res = "Capabilities:"; + // State: red + CloseWebSocketClient(); + } - Response2 cap = apiClient.GetCapabilities(token, "1"); - foreach (var item in cap.Capabilities) + private void CloseWebSocketClient() + { + if (websocketConnected) { - res += "\n" + item.TrackableType; + 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); } - Debug.Log("[REST] Capabilities: " + res); } #endregion @@ -94,57 +237,245 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface // public AskFrameRateResult SetPoseEstimationFramerate(string token, PoseConfigurationTrackableType type, EncodingInformationStructure encodingInformation, int minimumFramerate) { - return AskFrameRateResult.NOT_SUPPORTED; ///We cannot set any framerate for tracking on ARKit and ARCore + 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 = null; - return PoseEstimationResult.OK; + 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) { - poses = null; - return null; + 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) { - subscriptionUUID = Guid.Empty; + // 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) { - subscriptionUUIDs = null; - return null; + 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; - return InformationSubscriptionResult.OK; + + 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) { - return InformationSubscriptionResult.OK; + // 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) { - return InformationSubscriptionResult.OK; + 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) { - capabilities = null; - return CapabilityResult.OK; + 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) @@ -152,7 +483,21 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface isSupported = false; type = TypeWorldStorage.UNKNOWN; capability = null; - return CapabilityResult.OK; + + 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