Skip to content
Snippets Groups Projects
WorldAnalysisREST.cs 17.2 KiB
Newer Older
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using ETSI.ARF.OpenAPI.WorldAnalysis;
using ETSI.ARF.WorldAnalysis;
using ETSI.ARF.WorldAnalysis.REST;
//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)
        {
        }
    #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();
        // 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);
    #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.");
        }
    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)
        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;
        }