Skip to content
Snippets Groups Projects

Finalisation of REST and websocket client functions and subscription methodics.

Merged Sylvain Renault requested to merge feature/WAClientWithWebsockets into main
1 file
+ 381
36
Compare changes
  • Side-by-side
  • Inline
using System;
using System;
 
using System.Collections;
 
using System.Collections.Generic;
using UnityEngine;
using UnityEngine;
using ETSI.ARF.OpenAPI.WorldAnalysis;
using ETSI.ARF.OpenAPI.WorldAnalysis;
using ETSI.ARF.WorldAnalysis;
using ETSI.ARF.WorldAnalysis;
using static WorldAnalysisInterface;
using static WorldAnalysisInterface;
using ETSI.ARF.WorldAnalysis.REST;
using ETSI.ARF.WorldAnalysis.REST;
 
using WebSocketSharp;
 
//Implementation of the WorldAnalysis interface
//Implementation of the WorldAnalysis interface
public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
{
{
static protected string token = "ARF_Permission";
//
// Inspector variables
 
//
 
/// <summary>
 
/// WorldSAnalysisServer
 
/// </summary>
public WorldAnalysisServer waServer;
public WorldAnalysisServer waServer;
 
public string token = "ETSI-ARF-STF";
 
public string sessionID = "RESTful-API";
// For sync calls
[Space(8)]
private WorldAnalysisClient apiClient;
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
#region Unity_Methods
@@ -26,6 +57,11 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
@@ -26,6 +57,11 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
/// </summary>
/// </summary>
protected void Awake()
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
// sync
var httpClient = new BasicHTTPClient(waServer.URI);
var httpClient = new BasicHTTPClient(waServer.URI);
apiClient = new WorldAnalysisClient(httpClient);
apiClient = new WorldAnalysisClient(httpClient);
@@ -47,6 +83,12 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
@@ -47,6 +83,12 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
/// </summary>
/// </summary>
protected void Update()
protected void Update()
{
{
 
ManageSubscriptionValidity();
 
 
// todo: Call subscription callback(s) here or in the websocket?!?
 
foreach (KeyValuePair<Guid, SubscriptionInfo> subPose in m_subscriptionsPoses)
 
{
 
}
}
}
#endregion
#endregion
@@ -62,29 +104,130 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
@@ -62,29 +104,130 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
Debug.Log("[REST] WA Version: " + ver);
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();
return webSocket;
param.Mode = Mode_WorldAnalysis.DEVICE_TO_TRACKABLES;
}
param.Target = Guid.Parse("fa8bbe40-8052-11ec-a8a3-0242ac120002"); // test
SubscriptionSingle response = apiClient.SubscribeToPose(token, "1", param);
private void OnDestroy()
res = response.WebsocketUrl;
return res;
}
public void PrintCapabilities()
{
{
string res = "Capabilities:";
// State: red
 
CloseWebSocketClient();
 
}
Response2 cap = apiClient.GetCapabilities(token, "1");
private void CloseWebSocketClient()
foreach (var item in cap.Capabilities)
{
 
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
#endregion
@@ -94,57 +237,245 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
@@ -94,57 +237,245 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
//
//
public AskFrameRateResult SetPoseEstimationFramerate(string token, PoseConfigurationTrackableType type, EncodingInformationStructure encodingInformation, int minimumFramerate)
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)
public PoseEstimationResult GetLastPose(string token, Guid uuid, Mode_WorldAnalysis mode, out ETSI.ARF.OpenAPI.WorldAnalysis.Pose pose)
{
{
pose = null;
pose = apiClient.GetPose(token, sessionID, uuid, mode);
return PoseEstimationResult.OK;
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)
public PoseEstimationResult[] GetLastPoses(string token, Guid[] uuids, Mode_WorldAnalysis[] modes, out ETSI.ARF.OpenAPI.WorldAnalysis.Pose[] poses)
{
{
poses = null;
if (uuids.Length != modes.Length)
return null;
{
 
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)
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;
return InformationSubscriptionResult.OK;
}
}
public InformationSubscriptionResult[] SubscribeToPoses(string token, Guid[] uuids, Mode_WorldAnalysis[] modes, PoseCallback callback, ref int validity, out Guid[] subscriptionUUIDs)
public InformationSubscriptionResult[] SubscribeToPoses(string token, Guid[] uuids, Mode_WorldAnalysis[] modes, PoseCallback callback, ref int validity, out Guid[] subscriptionUUIDs)
{
{
subscriptionUUIDs = null;
if (uuids.Length != 0 && uuids.Length == modes.Length)
return null;
{
 
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)
public InformationSubscriptionResult GetSubsription(string token, Guid subscriptionUUID, out PoseCallback callback, out Guid target, out Mode_WorldAnalysis mode, out int validity)
{
{
 
// default
callback = null;
callback = null;
target = Guid.Empty;
target = Guid.Empty;
mode = Mode_WorldAnalysis.TRACKABLES_TO_DEVICE;
mode = Mode_WorldAnalysis.TRACKABLES_TO_DEVICE;
validity = 0;
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)
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)
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)
public CapabilityResult GetCapabilities(string token, out Capability[] capabilities)
{
{
capabilities = null;
Response2 cap = apiClient.GetCapabilities(token, sessionID);
return CapabilityResult.OK;
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)
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
@@ -152,7 +483,21 @@ public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
isSupported = false;
isSupported = false;
type = TypeWorldStorage.UNKNOWN;
type = TypeWorldStorage.UNKNOWN;
capability = null;
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
#endregion
Loading