Skip to content
Snippets Groups Projects
Commit ce716a98 authored by Sylvain Renault's avatar Sylvain Renault
Browse files

Merge branch 'feature/WAClientWithWebsockets' into 'main'

Finalisation of REST and websocket client functions and subscription methodics.

See merge request !2
parents 68788e54 866c2b4a
No related branches found
No related tags found
1 merge request!2Finalisation of REST and websocket client functions and subscription methodics.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using ETSI.ARF.OpenAPI.WorldAnalysis;
using ETSI.ARF.WorldAnalysis;
using static WorldAnalysisInterface;
using System;
using ETSI.ARF.WorldAnalysis.REST;
using UnityEngine.Events;
//Implementation of the WorldAnalysis interface
public class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
public partial class WorldAnalysisREST : MonoBehaviour, WorldAnalysisInterface
{
[Serializable]
public class StringEvent : UnityEvent<string> { }
//
// Inspector variables
//
// Name to register the client to the World Analysis
public string modulename = "UnityValidationApp";
/// <summary>
/// WorldSAnalysisServer
/// </summary>
public WorldAnalysisServer waServer;
public string token = "ETSI-ARF-STF";
public string sessionID = "ARF-STF669";
[Space(8)]
public bool isDebug = false;
//
// Private members
//
private WorldAnalysisClient apiClient; // For sync calls
private WorldAnalysisClient apiClientAsync; // For async calls
//
// 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> subscriptionsPoses;
public struct SubscriptionInfo
{
public ETSI.ARF.OpenAPI.WorldAnalysis.Pose pose;
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 PoseCallback callback;
}
#region Unity_Methods
......@@ -14,6 +64,18 @@ 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>();
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>
......@@ -28,74 +90,348 @@ 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 subscriptionsPoses)
{
}
}
private void OnDestroy()
{
WebSocketClient_Close();
}
#endregion
#region Test methods
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 Lifecycle
/// <summary>
/// Check the validity of all subscriptions and delete one if needed
/// </summary>
protected void ManageSubscriptionValidity()
{
if (subscriptionsPoses.Count == 0) return;
#region ARF_API
List<Guid> subscriptionToDelete = new List<Guid>();
foreach (KeyValuePair<Guid, SubscriptionInfo> sub in 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);
subscriptionsPoses.Remove(s);
}
}
#endregion
public AskFrameRateResult SetPoseEstimationFramerate(string token, PoseConfigurationTrackableType type, EncodingInformationStructure encodingInformation, int minimumFramerate)
#region ARF_API
//
// Implementation of the endpoints
//
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)
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<UuidAndMode> uuidList = new List<UuidAndMode>();
Poses poses_ = apiClient.GetPoses(token, sessionID, uuidList.ToArray());
List<ETSI.ARF.OpenAPI.WorldAnalysis.Pose> posesList = poses_.Poses1 as List<ETSI.ARF.OpenAPI.WorldAnalysis.Pose>;
if (poses_ != null && posesList != null && posesList.Count > 0)
{
for (int i = 0; i < uuids.Length; i++)
{
PoseEstimationResult poseResult = new PoseEstimationResult();
resul[i] = poseResult;
poses[i] = posesList[i];
}
return resul;
}
else
{
poses = null;
return null;
}
}
private string CreateWebsocket_URL(string originalURL)
{
poses = null;
return null;
string uri = originalURL;
//
// Overwrite server setting for testing local/extra servers
//
//if (isDebug) uri = "ws://localhost:61788/ws"; // for tests
//if (isDebug) uri = "wss://localhost:44301/ws"; // for tests
return uri;
}
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;
validity = response.Validity;
//SubscriptionMultipleRequest body_m = new SubscriptionMultipleRequest();
//body_m.Targets = new List<object>();
//body_m.Mode = new List<Mode_WorldAnalysis>();
//body_m.Validity = validity;
//body_m.WebhookUrl = callback == null ? "" : "https:\\..."; // empty -> app will use websockets (client)!
//SubscriptionMultiple response = apiClient.SubscribeToPose(token, sessionID, body_m);
// We add the subscription
SubscriptionInfo sub = new SubscriptionInfo();
sub.uuid = subscriptionUUID;
sub.uuidTarget = uuid;
sub.timeValidity = Time.time + (validity / 1000.0f);
sub.callback = callback;
sub.pose = new ETSI.ARF.OpenAPI.WorldAnalysis.Pose();
sub.pose.Mode = mode;
subscriptionsPoses.Add(sub.uuid, sub);
if (!string.IsNullOrEmpty(response.WebhookUrl))
{
// Create a REST server so that the WA server can send pose update to it
string webhookUrl = response.WebhookUrl;
// Close other communication channel
WebSocketClient_Close();
// How to auto-generate the C# REST webhook server for pose for Unity?
WebHookServer_Create(webhookUrl);
}
else if (!string.IsNullOrEmpty(response.WebsocketUrl))
{
// Create the WebSockets client here (NOT in the scene scripts)
// Close other communication channel
WebHookServer_Close();
// Open the websocket
if (webSocket == null) WebSocketClient_Create(CreateWebsocket_URL(response.WebsocketUrl)); // only one instance
}
else throw new Exception("[REST] No valid 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)
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 (subscriptionsPoses.ContainsKey(subscriptionUUID))
{
// Check the server subscription
SubscriptionSingle sub = apiClient.GetSubscription(token, sessionID, subscriptionUUID);
// Check local one
if (sub.Uuid == subscriptionUUID)
{
SubscriptionInfo subInfo = 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 (subscriptionsPoses.ContainsKey(subscriptionUUID))
{
SubscriptionInfo sub = subscriptionsPoses[subscriptionUUID];
PoseCallback oldCB = sub.callback;
Body newSub = new Body();
newSub.Mode = mode;
newSub.Validity = validity;
newSub.WebhookUrl = callback == null ? "" : "https:\\..."; // empty -> app will use websockets (client)!
// Update subscription info in the REST server
SubscriptionSingle response = apiClient.UpdateSubscription(token, sessionID, subscriptionUUID, newSub);
// 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))
{
// Create a REST server so that the WA server can send pose update to it
string webhookUrl = response.WebhookUrl;
// Close other communication channel
WebSocketClient_Close();
// How to auto-generate the C# REST server for pose for Unity?
WebHookServer_Create(webhookUrl);
}
else if (oldCB == null && callback != null && !string.IsNullOrEmpty(response.WebsocketUrl))
{
// Create the WebSockets client here (NOT in the scene scripts)
// Close other communication channel
WebHookServer_Close();
// Open the websocket?
if (webSocket == null) WebSocketClient_Create(CreateWebsocket_URL(response.WebsocketUrl));
}
else throw new Exception("[REST] No valid URL in server reponse.");
return InformationSubscriptionResult.OK;
}
return InformationSubscriptionResult.UNKNOWN_ID;
}
public InformationSubscriptionResult UnSubscribeToPose(Guid subscriptionUUID)
public InformationSubscriptionResult UnsubscribeFromPose(Guid subscriptionUUID)
{
return InformationSubscriptionResult.OK;
if (subscriptionsPoses.ContainsKey(subscriptionUUID))
{
apiClient.UnsubscribeFromPose(token, sessionID, subscriptionUUID);
subscriptionsPoses.Remove(subscriptionUUID);
if (subscriptionsPoses.Count == 0)
{
// Close the connection via websockets
WebSocketClient_Close();
}
return InformationSubscriptionResult.OK;
}
return InformationSubscriptionResult.UNKNOWN_ID;
}
public CapabilityResult GetCapabilities(string token, out Capability[] capabilities)
{
capabilities = null;
return CapabilityResult.OK;
Capabilities cap = apiClient.GetCapabilities(token, sessionID);
if (cap == null || cap.Capabilities1 == null || cap.Capabilities1.Count == 0)
{
capabilities = null;
return CapabilityResult.FAIL;
}
else
{
capabilities = new Capability[cap.Capabilities1.Count];
cap.Capabilities1.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)
{
isSupported = false;
type = TypeWorldStorage.UNKNOWN;
capability = null;
return CapabilityResult.OK;
Supports 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
......
#define IS_HHI_LAN
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Newtonsoft.Json;
using WebSocketSharp;
using ETSI.ARF.OpenAPI.WorldAnalysis;
using static WorldAnalysisInterface;
//Implementation of the WorldAnalysis interface
public partial class WorldAnalysisREST
{
//
// Inspector variables
//
public StringEvent webSocketMessage;
//
// Private members
//
private WebSocketSharp.WebSocket webSocket = null; // For WebSockets
#region Communication system for WebSockets
public WebSocket WebSocketClient_Create(string url)
{
if (webSocket != null) return webSocket;
string _url = url;
#if IS_HHI_LAN
_url = "ws://192.168.20.29:8084/ws";
Debug.LogWarning("[WS] Changing the websocket URL to: " + _url + " (local network VM, HHI)");
#endif
webSocket = new WebSocketSharp.WebSocket(_url);
//
// Define standard callbacks
//
webSocket.OnOpen += (sender, e) =>
{
Debug.Log("[WS] Connected");
webSocket.Send("RegisterClient=" + modulename);
};
webSocket.OnClose += (sender, e) =>
{
Debug.Log("[WS] Disconnected");
};
webSocket.OnMessage += (sender, e) => WebSocketClient_OnReceive(e.Data);
webSocket.OnError += (sender, e) => Debug.Log("[WS] Websocket error!");
webSocket.Connect();
return webSocket;
}
private void WebSocketClient_Close()
{
if (webSocket != null)
{
webSocket.Send("UnregisterClient=" + modulename);
webSocket.Close();
webSocket = null;
}
}
public void WebSocketClient_Send(string msg)
{
webSocket?.Send(msg);
}
bool isRegistered = false;
public void WebSocketClient_OnReceive(string serverMessage)
{
//Debug.Log("[WS] Receiving: " + data);
if (serverMessage.Contains("You are now registered"))
{
isRegistered = true;
Debug.Log($"[WS] {serverMessage }");
//Debug.Log($"[WS] Registration of { modulename } was succesfull.");
}
else if (isRegistered)
{
if (serverMessage == "PoseStop")
{
//SetColor(Color.yellow);
}
else if (serverMessage == "PoseIsNowSubscribed")
{
}
else if (serverMessage == "PoseIsNowUnubscribed")
{
}
else if (serverMessage.StartsWith("NewPose=") && serverMessage.Contains("estimationState"))
{
// Handle the new pose
string _str = serverMessage.Substring("NewPose=".Length);
ETSI.ARF.OpenAPI.WorldAnalysis.Pose pose = JsonConvert.DeserializeObject<ETSI.ARF.OpenAPI.WorldAnalysis.Pose>(_str);
//Debug.Log("[WS] JSON - my new pose : " + pose.ToJson());
// to check: p.Confidence?
PoseEstimationResult res = pose.EstimationState == PoseEstimationState.OK ? PoseEstimationResult.OK : PoseEstimationResult.FAILURE;
// Look for the corresponding callbacks
foreach (var item in subscriptionsPoses.Values)
{
if (item.uuidTarget == pose.Uuid)
{
item.pose.Value = pose.Value;
item.callback(res, pose);
}
}
}
else webSocketMessage?.Invoke(serverMessage);
}
}
#endregion
}
\ No newline at end of file
fileFormatVersion: 2
guid: 7b7bcc535a949e24283db7e23c51dc46
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
using System;
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 partial class WorldAnalysisREST
{
//
// Inspector variables
//
public StringEvent webhookMessage;
private bool webhookRunning = false;
#region Communication system for WebHooks
private void WebHookServer_Create(string url)
{
webhookRunning = true;
throw new Exception("[API] WebHookServer_Create(): Not implemented!");
}
private void WebHookServer_Close()
{
if (webhookRunning)
{
webhookRunning = false;
throw new Exception("[API] WebHookServer_Close(): Not implemented!");
}
}
private object WebHookServer_OnReceive()
{
throw new Exception("[API] WebHookServer_OnReceive(): Not implemented!");
}
private void WebHookServer_Send(object message)
{
throw new Exception("[API] WebHookServer_Send(): Not implemented!");
}
#endregion
}
\ No newline at end of file
fileFormatVersion: 2
guid: 67d4b99a5f7593e4a857b6c88249d564
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
......@@ -33,7 +33,7 @@ public class WorldAnalysisUnityHelper
/// </summary>
/// <param name="matrix">the values to convert</param>
/// <returns>Converted Unity Matrix</returns>
public static Matrix4x4 ConvertETSIARFTransform3DToUnity(ETSI.ARF.OpenAPI.WorldStorage.Transform3D value)
public static Matrix4x4 ConvertETSIARFTransform3DToUnity(ETSI.ARF.OpenAPI.WorldAnalysis.Transform3D value)
{
if (value.Count == 16)
{
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment