diff --git a/openapi b/openapi new file mode 160000 index 0000000000000000000000000000000000000000..073fd7213fd9e6ebc2f8a47d628a650de30c8bc4 --- /dev/null +++ b/openapi @@ -0,0 +1 @@ +Subproject commit 073fd7213fd9e6ebc2f8a47d628a650de30c8bc4 diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Dockerfile b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Dockerfile index 1203a89ba3579798b30bb4b67e45538b2c42d7b5..76f44e25130c34acda31ffe342b389729b530da1 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Dockerfile +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Dockerfile @@ -1,21 +1,21 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. # Container we use for final publish -FROM mcr.microsoft.com/dotnet/core/aspnet:5.0-buster-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base WORKDIR /app EXPOSE 80 EXPOSE 443 # Build container -FROM mcr.microsoft.com/dotnet/core/sdk:5.0-buster AS build +FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build # Copy the code into the container WORKDIR /src -COPY ["src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI.ARF.OpenAPI.WorldAnalysis.csproj", "ETSI.ARF.OpenAPI.WorldAnalysis/"] +COPY ["ETSI.ARF.OpenAPI.WorldAnalysis.csproj", "ETSI.ARF.OpenAPI.WorldAnalysis/"] # NuGet restore RUN dotnet restore "ETSI.ARF.OpenAPI.WorldAnalysis/ETSI.ARF.OpenAPI.WorldAnalysis.csproj" -COPY ["src/ETSI.ARF.OpenAPI.WorldAnalysis/", "ETSI.ARF.OpenAPI.WorldAnalysis/"] +COPY [".", "ETSI.ARF.OpenAPI.WorldAnalysis/"] # Build the API WORKDIR "ETSI.ARF.OpenAPI.WorldAnalysis" diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/CapabilitiesImpl.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/CapabilitiesImpl.cs index 3529f665f4d9f1d980521bc88507f96fb613d453..4638d5c7df96786128abcc49291f699338163b95 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/CapabilitiesImpl.cs +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/CapabilitiesImpl.cs @@ -53,7 +53,22 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult GetCapabilities([FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) { - return StatusCode(405, "Not supported yet!"); + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID + + // Get all capabilities from all anchors/trackables + // Info: Capabilities are collected after a module is connected via websockets + + // Create list + List capabilitiesList = new List(); + capabilitiesList.AddRange(WorldAnalysisConnections.Singleton.GetCapabilities()); + + // Create response object + GetCapabilities200Response response = new GetCapabilities200Response(); + response.Capabilities = capabilitiesList; + return new ObjectResult(response); + //return StatusCode(405, "Not supported yet!"); } /// @@ -61,7 +76,19 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult GetSupport([FromRoute (Name = "trackableOrAnchorUUID")][Required]Guid trackableOrAnchorUUID, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) { - return StatusCode(405, "Not supported yet!"); + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID + + // Create list + List capabilitiesList = new List(); + capabilitiesList.AddRange(WorldAnalysisConnections.Singleton.GetCapabilitiesFromUuid(trackableOrAnchorUUID)); + + // Create response object + GetSupport200Response response = new GetSupport200Response(); + response.Capabilities = capabilitiesList; + return new ObjectResult(response); + //return StatusCode(405, "Not supported yet!"); } } } diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/PoseImpl.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/PoseImpl.cs index 93f84bf5e40afd68daafb623344f0a90954c507e..44606416aab46dfd8512ffebd5068f2ada13de84 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/PoseImpl.cs +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/PoseImpl.cs @@ -41,66 +41,212 @@ using ETSI.ARF.OpenAPI.WorldAnalysis.Attributes; using ETSI.ARF.OpenAPI.WorldAnalysis.Models; namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers -{ +{ /// /// /// [ApiController] public class PoseApiControlleImpl : PoseApiController - { + { /// /// Specify the a minimum frame rate for pose estimation for Trackable types /// - public override IActionResult ConfigureFramerate([FromBody]PoseConfiguration poseConfiguration, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) + public override IActionResult ConfigureFramerate([FromBody] PoseConfiguration poseConfiguration, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - return StatusCode(405, "Not supported yet!"); + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID + + // Notify the modules that the client need a new framerate + bool result = WorldAnalysisConnections.Singleton.ConfigureFramerate(poseConfiguration); + return result ? StatusCode(200, "Ok.") : StatusCode(405, "Not supported."); } /// /// Request the last pose of a single Anchor or Trackable /// - public override IActionResult GetPose([FromRoute (Name = "trackableOrAnchorUUID")][Required]Guid trackableOrAnchorUUID, [FromQuery (Name = "mode")][Required()]ModeWorldAnalysis mode, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) + public override IActionResult GetPose([FromRoute(Name = "trackableOrAnchorUUID")][Required] Guid trackableOrAnchorUUID, [FromQuery(Name = "mode")][Required()] ModeWorldAnalysis mode, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - return StatusCode(405, "Not supported yet!"); + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID + + // Request from the modules a new pose of a single UUID + Pose result; + result = WorldAnalysisConnections.Singleton.GetPose(trackableOrAnchorUUID, mode); + return new ObjectResult(result); } /// /// Request the last pose of a batch of Anchor or Trackable /// - public override IActionResult GetPoses([FromQuery (Name = "uuid")][Required()]List uuid, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) + public override IActionResult GetPoses([FromQuery(Name = "uuid")][Required()] List uuid, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - return StatusCode(405, "Not supported yet!"); + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID + + // Request from the modules new poses from all UUIDs + GetPoses200Response result = new GetPoses200Response(); + foreach (var item in uuid) + { + result.Poses.Add(WorldAnalysisConnections.Singleton.GetPose(item.Uuid, item.Mode)); + } + return new ObjectResult(result); } +#pragma warning disable CS1591 // Fehlendes XML-Kommentar für öffentlich sichtbaren Typ oder Element + + // + // Management of subscriptions + // + /// + /// Dictionnary of susbscription informations for poses, for each item, stored using the UUID of the item (anchor/trackable) + /// + private Dictionary m_subscriptionsPoses = new Dictionary(); + + public struct SubscriptionInfo + { + public Guid uuidSub; // id of subscription (id is defined by the WA server) + + public SubscribeToPoseRequest subscription; + public string webSocket; + public Pose pose; + public DateTime timeValidity; + + //public PoseCallback callback; + } +#pragma warning restore CS1591 // Fehlendes XML-Kommentar für öffentlich sichtbaren Typ oder Element + /// /// Get information about a subscription /// - public override IActionResult GetSubscription([FromRoute (Name = "subscriptionUUID")][Required]Guid subscriptionUUID, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) + public override IActionResult GetSubscription([FromRoute(Name = "subscriptionUUID")][Required] Guid subscriptionUUID, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - return StatusCode(405, "Not supported yet!"); + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID + + // todo: search for a subscription and send it back + foreach (var item in m_subscriptionsPoses) + { + if (item.Key == subscriptionUUID) + { + SubscriptionSingle response = new SubscriptionSingle(); + response.Uuid = subscriptionUUID; + response.Target = item.Value.subscription.Target; + response.Mode = item.Value.pose.Mode; + response.Validity = item.Value.timeValidity.Millisecond; + response.WebhookUrl = item.Value.subscription.WebhookUrl; + response.WebsocketUrl = item.Value.webSocket; + return new ObjectResult(response); + } + } + return StatusCode(404, "Not found."); } /// /// Subscribe to collect the pose of an AR device, an Anchor or a Trackable /// - public override IActionResult SubscribeToPose([FromBody]SubscribeToPoseRequest subscribeToPoseRequest, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) + public override IActionResult SubscribeToPose([FromBody] SubscribeToPoseRequest subscribeToPoseRequest, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - return StatusCode(405, "Not supported yet!"); + // Simulation ON for STF669 pose calcualtion + string _token = token; + Guid parentUUID = Guid.Empty; + if (token.Contains(",")) + { + _token = token.Split(',')[0]; + parentUUID = new Guid(token.Split(',')[1]); + } + + if (!Startup.IsAccessGranted(_token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID + + if (subscribeToPoseRequest.Targets != null && subscribeToPoseRequest.Targets.Count > 0) + { + return StatusCode(404, "Multiple subscriptions are not implemented (targets)."); + } + else if (subscribeToPoseRequest.Modes != null && subscribeToPoseRequest.Modes.Count > 0) + { + return StatusCode(404, "Multiple subscriptions are not implemented (modes)."); + } + + int validity = subscribeToPoseRequest.Validity; // todo: is to handle here or by the client? + + SubscribeToPose200Response response = new SubscribeToPose200Response(); + response.Validity = validity; + response.Uuid = Guid.NewGuid(); + response.Target = subscribeToPoseRequest.Target; + response.Mode = subscribeToPoseRequest.Mode; + response.WebhookUrl = subscribeToPoseRequest.WebhookUrl; + response.WebsocketUrl = ""; + + // Send the websocket connection URL + if (string.IsNullOrEmpty(response.WebhookUrl)) + { + // Notice: starting websocket server is done autom. by the client, when calling "URL:xxx/ws" + // Registering the client is done in the analysis module, so the websocket can send to it pose updates + response.WebsocketUrl = "wss://" + Request.Host.ToString() + "/ws"; + } + + // We add the subscription + SubscriptionInfo info = new SubscriptionInfo(); + info.uuidSub = response.Uuid; + info.webSocket = response.WebsocketUrl; + info.timeValidity = DateTime.Now.AddMilliseconds(validity / 1000.0f); + + info.pose = new Pose(); + info.pose.Mode = response.Mode; + + info.subscription = new SubscribeToPoseRequest(); + info.subscription.Target = response.Target; + info.subscription.Mode = response.Mode; + info.subscription.Validity = response.Validity; + info.subscription.WebhookUrl = response.WebhookUrl; + + m_subscriptionsPoses.Add(info.uuidSub, info); + + // todo: inform the module(s) that the client will track an anchor/trackable and need the pose + // todo: has the module to call GetRelocalizationInformation() then?!? + WorldAnalysisConnections.Singleton.SubscribeToPose(info, parentUUID); + + return new ObjectResult(response); } /// /// Remove a subscription to a given pose /// - public override IActionResult UnsubscribeFromPose([FromRoute (Name = "subscriptionUUID")][Required]Guid subscriptionUUID, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) + public override IActionResult UnsubscribeFromPose([FromRoute(Name = "subscriptionUUID")][Required] Guid subscriptionUUID, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - return StatusCode(405, "Not supported yet!"); + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID + + // Remove the subscription from the list? + if (m_subscriptionsPoses.ContainsKey(subscriptionUUID)) m_subscriptionsPoses.Remove(subscriptionUUID); + else + { + //return StatusCode(404, new Error() { Message = "Unsubscribe UUID not found!" }); + } + + // Inform the module(s) that the subscription ended + WorldAnalysisConnections.Singleton.UnsubscribeFromPose(subscriptionUUID); + + return StatusCode(200, new Success() { Message = "Unsubscription successfull." }); } /// /// Update a subscription /// - public override IActionResult UpdateSubscription([FromRoute (Name = "subscriptionUUID")][Required]Guid subscriptionUUID, [FromBody]UpdateSubscriptionRequest updateSubscriptionRequest, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) + public override IActionResult UpdateSubscription([FromRoute(Name = "subscriptionUUID")][Required] Guid subscriptionUUID, [FromBody] UpdateSubscriptionRequest updateSubscriptionRequest, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID + + // todo: inform the module(s) that the subscription changed + return StatusCode(405, "Not supported yet!"); } } diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/WebSocketController.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/WebSocketController.cs index 6e80b54d679b0ac6e03429de8246451ca5e6ab9d..47315344237a9b087410b0bca2610e1fdb2a7444 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/WebSocketController.cs +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ControllersImpl/WebSocketController.cs @@ -30,9 +30,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.SwaggerGen; +using Newtonsoft.Json; + +using ETSI.ARF.OpenAPI.WorldAnalysis.Models; #pragma warning disable CS1591 // Fehlendes XML-Kommentar für öffentlich sichtbaren Typ oder Element -namespace ETSI.ARF.OpenAPI.WorldStorage.Services +namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers { // // ETSI-ARF World Analysis WebSocket implementation @@ -40,6 +43,14 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services // public class WebSocketController : ControllerBase { + private WebSocket websocket; + static public int WebSocketControllerInstanceCount = 0; + + private string currentName = ""; + private bool registered = false; + private bool firstTime = true; + private int timeCnt = 3; + [HttpGet("/ws")] public async Task Get() { @@ -47,12 +58,15 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services { using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); - if (webSocket.State == WebSocketState.Open) + if (webSocket.State == WebSocketState.Connecting) { - // Response an OK message - await SendText(webSocket, "Hello, here is the ARF World Analysis services!"); + // Response an OK message? + } + else if (webSocket.State == WebSocketState.Open) + { + // Go to the loop... + await WebSocketServer_Loop(HttpContext, webSocket); } - await Echo(HttpContext, webSocket); } else { @@ -64,59 +78,298 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services // // Send a line of text // - private async Task SendText(WebSocket webSocket, string text) + static private async Task SendText(WebSocket ws, string msg) { - // Response an OK message - var message = text; + var message = msg; var bytes = Encoding.UTF8.GetBytes(message); var arraySegment = new ArraySegment(bytes, 0, bytes.Length); - await webSocket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None); + await ws.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None); + } + + public void SendText(string msg) + { + var message = msg; + var bytes = Encoding.UTF8.GetBytes(message); + var arraySegment = new ArraySegment(bytes, 0, bytes.Length); + websocket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None); } // - // Send the time all seconds + // Send a demo pose all seconds // - private async Task SendTime(WebSocket webSocket) + float rotInc = 0; + private async Task SendDemoPose(WebSocket ws) { while (true) { - var message = "Hello, my time is: " + DateTime.Now.ToLocalTime(); - var bytes = Encoding.UTF8.GetBytes(message); - var arraySegment = new ArraySegment(bytes, 0, bytes.Length); - - if (webSocket.State == WebSocketState.Open) + if (ws.State == WebSocketState.Open) { - await webSocket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None); + timeCnt--; + if (timeCnt < 0) + { + await SendText(ws, "PoseStop"); + break; + } + else + { + PoseValue val = new PoseValue(); + val.Unit = UnitSystem.MEnum; + val.Position = new List() { 1, 2, 3 }; + val.Rotation = new List() { rotInc, 0, 0, 0 }; + rotInc += 0.01f; + + Pose pose = new Pose(); + pose.Uuid = new Guid(); + pose.EstimationState = Pose.EstimationStateEnum.OKEnum; + //pose.SubscriptionUrl = ""; + pose.Value = val; + pose.Timestamp = (int)DateTime.Now.ToFileTime(); + pose.Confidence = 1; + pose.Mode = ModeWorldAnalysis.DEVICETOTRACKABLESEnum; + + string json = pose.ToJson(); + + await SendText(ws, "NewPose=" + json); + } } - else if (webSocket.State == WebSocketState.Closed || webSocket.State == WebSocketState.Aborted) + else if (ws.State == WebSocketState.Closed || ws.State == WebSocketState.Aborted) { + if (WorldAnalysisConnections.Singleton.modules.Contains(currentModule)) WorldAnalysisConnections.Singleton.modules.Remove(currentModule); + if (WorldAnalysisConnections.Singleton.clients.Contains(currentClient)) WorldAnalysisConnections.Singleton.clients.Remove(currentClient); + currentClient = null; + currentModule = null; break; } - Thread.Sleep(1000); + Thread.Sleep(250); } } + private Module currentModule; + private Client currentClient; + + private async void OnReceiveText(string msg) + { + #region Register the client/module + if (firstTime) + { + if (msg.StartsWith("RegisterModule=")) + { + registered = true; + firstTime = false; + currentName = msg.Split('=')[1]; + + // If module exist already reuse it + Module module = null; + foreach (var item in WorldAnalysisConnections.Singleton.modules) if (item.name == currentName) module = item; + if (module != null) + { + currentModule = module; + currentModule.name = currentName; + currentModule.websockets = this; + currentModule.capabilities.Clear(); + } + + if (currentModule == null && currentClient == null) + { + currentModule = new Module(); + currentModule.name = currentName; + currentModule.websockets = this; + WorldAnalysisConnections.Singleton.modules.Add(currentModule); + } + SendText($"ARF World Analysis Server: #{ WebSocketControllerInstanceCount } You are now registered as module: { currentName }"); + } + else if (msg.StartsWith("RegisterClient=")) + { + registered = true; + firstTime = false; + currentName = msg.Split('=')[1]; + if (currentModule == null && currentClient == null) + { + currentClient = new Client(); + currentClient.name = currentName; + currentClient.websockets = this; + WorldAnalysisConnections.Singleton.clients.Add(currentClient); + SendText($"ARF World Analysis Server: #{ WebSocketControllerInstanceCount } You are now registered as client: { currentName }"); + } + } + else + { + registered = false; + SendText("ARF World Analysis Server: Cannot register " + msg); + } + return; + } + #endregion + + if (registered) + { + // + // Some admin stuffs + // + if (msg == "Idle") + { + SendText("Idle"); + } + else if (msg == "Busy") + { + SendText("Busy"); + } + else if (msg.StartsWith("UnregisterModule=")) + { + string name = msg.Split('=')[1]; + + // Unregister a client (e.g. Unity client) + Module module = null; + foreach (var item in WorldAnalysisConnections.Singleton.modules) if (item.name == name) module = item; + if (module != null) + { + WorldAnalysisConnections.Singleton.modules.Remove(module); + SendText("UnregisterModuleOK"); + } + currentName = ""; + firstTime = true; + registered = false; + } + else if (msg.StartsWith("UnregisterClient=")) + { + string name = msg.Split('=')[1]; + + // Unregister a client (e.g. Unity client) + Client client = null; + foreach (var item in WorldAnalysisConnections.Singleton.clients) if (item.name == name) client = item; + if (client != null) + { + WorldAnalysisConnections.Singleton.clients.Remove(client); + SendText("UnregisterClientOK"); + } + currentName = ""; + firstTime = true; + registered = false; + } + // + // OpenAPI + // + else if (msg == "PoseIsNowSubscribed") + { + SendText("RequestNextPose"); + } + + // + // Messages from a module (Analysis) + // + else if (msg.StartsWith("Capabilities=")) // Receive capab. from a module + { + // Module is sending their capabilities + string[] str_cap = msg.Split('='); + string moduleName = str_cap[1]; + Capability _c = JsonConvert.DeserializeObject(str_cap[2]); + + // Has the module already send their capabilities? + Module module = null; + foreach (var item in WorldAnalysisConnections.Singleton.modules) if (item.name == moduleName) module = item; + if (module != null) module.capabilities.Add(_c); + } + else if (msg.StartsWith("NewPose=")) + { + string[] str_pose = msg.Split('='); + + // Send the pose to the client(s) + Pose pose = JsonConvert.DeserializeObject(str_pose[1]); + + //WorldAnalysisConnections.Singleton.SendPoseToClients(pose); + WorldAnalysisConnections.Singleton.SendPoseToClients(str_pose[1]); + + // todo: if there are some subscription ask modules for new pose + SendText("RequestNextPose"); + } + else if (msg.StartsWith("Error=")) + { + string errorMsg = msg.Split('=')[1]; + foreach (var item in WorldAnalysisConnections.Singleton.clients) + { + await SendText(item.websockets.websocket, errorMsg); + } + } + + // + // Messages from a client (Unity) + // + else if (msg.StartsWith("PoseStart")) // send some fake poses to Unity clients + { + await SendDemoPose(websocket); + } + else if (msg.StartsWith("CurrentUserPose=")) + { + // A client (Unity) is sending the current user posistion + + // Send new user pose to all modules + foreach (var item in WorldAnalysisConnections.Singleton.modules) + { + await SendText(item.websockets.websocket, msg); + } + + //// Test: send user pose back to clients + //string[] str_pose = msg.Split('='); + //Pose pose = JsonConvert.DeserializeObject(str_pose[1]); + //pose.Value.Position[2] += 1f; + //WorldAnalysisConnections.Singleton.SendPoseToClients(pose); + } + + // + // Messages from modules and clients + // + else + { + // Send a response + SendText("ARF World Analysis Server: I got this unknown message: " + msg); + } + } + } + + private async Task WebSocketServer_Loop(HttpContext context, WebSocket ws) + { + websocket = ws; + WebSocketControllerInstanceCount++; + + var buffer = new byte[1024 * 4]; + + // Read/get the first data block + WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + if (result.MessageType == WebSocketMessageType.Text) + { + string getMsg = System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); + OnReceiveText(getMsg); + } + + // Entering the loop + while (!result.CloseStatus.HasValue) + { + // Read/get the next block + result = await ws.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + if (result.MessageType == WebSocketMessageType.Text) + { + string getMsg = System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); + OnReceiveText(getMsg); + } + } + await ws.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + WebSocketControllerInstanceCount--; + } + + // // Echo incoming messages // - private async Task Echo(HttpContext context, WebSocket webSocket) + private async Task Echo(WebSocketReceiveResult context, WebSocket webSocket) { var buffer = new byte[1024 * 4]; - // get the first data block + // Read/get the first data block WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - while (!result.CloseStatus.HasValue) - { - // test - await SendText(webSocket, "Thanks, I got this message:"); - // echo the message back to the client - await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); - - // get the next block - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - } - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + // Send/echo the message back to the client + await SendText(webSocket, "ARF World Analysis Server: I got this (raw) message: "); + await webSocket.SendAsync(new ArraySegment(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None); } } } diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ModelsExt/NotNeeded.txt b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ModelsExt/NotNeeded.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/NoMongoDBneeded.txt b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/NoMongoDBneeded.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/Services/NotNeeded.txt b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/Services/NotNeeded.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisConnections.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisConnections.cs new file mode 100644 index 0000000000000000000000000000000000000000..c864547b3750de69a96ae41035c7a6f3c4788622 --- /dev/null +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisConnections.cs @@ -0,0 +1,134 @@ +using ETSI.ARF.OpenAPI.WorldAnalysis.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using static ETSI.ARF.OpenAPI.WorldAnalysis.Controllers.PoseApiControlleImpl; +using ETSI.ARF.OpenAPI.WorldAnalysis.Controllers; + +#pragma warning disable CS1591 // Fehlendes XML-Kommentar für öffentlich sichtbaren Typ oder Element +namespace ETSI.ARF.OpenAPI.WorldAnalysis +{ + // For management of WA modules + public class Module + { + public string name; + public List capabilities = new List(); + public WebSocketController websockets; + } + + // For management of WA client (e.g. Unity client) + public class Client + { + public string name; + public string session; + public VectorQuaternionPoseValue currentUserPose; + public WebSocketController websockets; + } + + public class WorldAnalysisConnections + { + static public WorldAnalysisConnections Singleton = new WorldAnalysisConnections(); + + public List modules = new List(); + public List clients = new List(); // e.g. Unity clients + + // todo + // Manage the sessions and modules + // get capabilities of all modules + public List GetCapabilities() + { + List list = new List(); + foreach (var item in modules) + { + list.AddRange(item.capabilities); + } + return list; + } + + public List GetCapabilitiesFromUuid(Guid trackableOrAnchorUUID) + { + List list = new List(); + foreach (var item in modules) + { + // todo: Check if uuid has the capability? + // Get the world object from the world storage via the module? + // Use GetRelocalisation() ? + list.AddRange(item.capabilities); + } + return list; + } + + public bool ConfigureFramerate(PoseConfiguration poseConfiguration) + { + bool ok = false; + foreach (var m in modules) + { + bool moduleHasCap = false; + foreach (var c in m.capabilities) + { + if ((int)poseConfiguration.TrackableType == (int)c.TrackableType) + { + moduleHasCap = true; + break; + } + } + // Configure the module via websocket + if (moduleHasCap) m.websockets.SendText("ConfigureFramerate=" + poseConfiguration.ToJson()); + } + return ok; + } + + public Pose GetPose(Guid trackableOrAnchorUUID, ModeWorldAnalysis mode) + { + Pose result = new Pose(); + foreach (var item in modules) + { + // todo: get the pose via websocket + item.websockets.SendText("GetPose=" + trackableOrAnchorUUID + "=" + mode.ToString()); + + // todo: find the pose with the best confidence? + // How to get the results !?!?! (List with results, per Modules) + } + return result; + } + + public void SubscribeToPose(SubscriptionInfo info, Guid parentTrackableUUID) + { + // Send to all modules a request of subscription + foreach (var item in modules) + { + if (parentTrackableUUID == Guid.Empty) item.websockets.SendText("SubscribePose=" + info.uuidSub + "=" + info.subscription.ToJson()); + else item.websockets.SendText("SubscribeSimulatedPose=" + info.uuidSub + "=" + info.subscription.ToJson() + "=" + parentTrackableUUID.ToString()); + } + } + + public void UnsubscribeFromPose(Guid subscriptionUUID) + { + // Send to all modules a request of subscription + foreach (var item in modules) + { + item.websockets.SendText("UnsubscribePose=" + subscriptionUUID.ToString()); + } + } + + public void SendPoseToClients(Pose pose) + { + // Send to all clients with valid subscription the new pose + foreach (var item in clients) + { + item.websockets.SendText("NewPose=" + pose.ToJson()); + } + } + + public void SendPoseToClients(string str_pose) + { + // Send to all clients with valid subscription the new pose + foreach (var item in clients) + { + item.websockets.SendText("NewPose=" + str_pose); + } + } + } +} +#pragma warning restore CS1591 // Fehlendes XML-Kommentar für öffentlich sichtbaren Typ oder Element diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Program.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Program.cs index 95e22062027cf8674a23366f548a3e07c48c5d15..1fba73140c56dfc242fd22c8b795c3bf7a51801f 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Program.cs +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Program.cs @@ -27,7 +27,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup() - .UseUrls("http://0.0.0.0:8080/"); + .UseUrls("http://0.0.0.0:44301/"); // SylR: Wichtig!!! }); } } diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs index 48be55250c2022d50514a393758b36234e0102d7..2a3f67c8c81faf23840f8f5e81c90a997d5cb9ba 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs @@ -15,7 +15,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// Last change: June 2024 +// Last change: September 2024 // /* @@ -23,7 +23,7 @@ * * API ensuring interoperability between Scene Management and a World Analysis service * - * The version of the OpenAPI document: 2.0.1 + * The version of the OpenAPI document: 2.0.2 * * Generated by: https://openapi-generator.tech */ @@ -64,17 +64,18 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis /// /// The API version. (how to read it from the yaml?) /// - static public string apiVersion = "2.0.1"; + static public string apiVersion = "2.0.0"; /// /// Demo access key /// - static public string accessKey = "My!Key.ETSI"; + static public string accessKey = "ARF"; /// /// Demo secret key /// - static public string secretKey = "GW0Wae1t4Cs5rAqEbPYFWO9J5nSbpJXxp1F3uv0J"; + //static public string secretKey = "GW0Wae1t4Cs5rAqEbPYFWO9J5nSbpJXxp1F3uv0J"; + static public string secretKey = "hhi"; /// /// Constructor @@ -90,6 +91,20 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis /// public IConfiguration Configuration { get; } + /// + /// SylR: Check if the request is authorized + /// + /// + /// + static public bool IsAccessGranted(string token) + { + if (token == null) return false; + else if (token == "dev") return true; // developermode + + string[] t = token.Split('&'); + return t[0] == accessKey && t[1] == secretKey; + } + /// /// This method gets called by the runtime. Use this method to add services to the container. /// @@ -116,7 +131,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis { c.EnableAnnotations(enableAnnotationsForInheritance: true, enableAnnotationsForPolymorphism: true); - c.SwaggerDoc("2.0.1", new OpenApiInfo + c.SwaggerDoc(apiVersion, new OpenApiInfo { Title = "World Analysis API", Description = "World Analysis API (ASP.NET Core 5.0)", @@ -132,7 +147,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis Name = "NoLicense", Url = new Uri("https://opensource.org/licenses/BSD-3-Clause") }, - Version = "2.0.1", + Version = apiVersion, }); c.CustomSchemaIds(type => type.FriendlyId(true)); c.IncludeXmlComments($"{AppContext.BaseDirectory}{Path.DirectorySeparatorChar}{Assembly.GetEntryAssembly().GetName().Name}.xml"); @@ -161,14 +176,15 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis app.UseHsts(); } - // ETSI-ARF Websocket implementation + // ETSI-ARF Websockets implementation var webSocketOptions = new WebSocketOptions() { - KeepAliveInterval = TimeSpan.FromSeconds(120), + //KeepAliveInterval = TimeSpan.FromSeconds(120), + //AllowedOrigins.Add("https://etsi.hhi.fraunhofer.de"), + //AllowedOrigins.Add("https://www.client.com") }; - //webSocketOptions.AllowedOrigins.Add("https://etsi.hhi.fraunhofer.de"); - //webSocketOptions.AllowedOrigins.Add("https://www.client.com"); - app.UseWebSockets(); + app.UseWebSockets(webSocketOptions); + //app.UseWebSockets(); app.UseHttpsRedirection(); app.UseDefaultFiles(); @@ -182,7 +198,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis // set route prefix to openapi, e.g. http://localhost:8080/openapi/index.html c.RoutePrefix = "openapi"; //TODO: Either use the SwaggerGen generated OpenAPI contract (generated from C# classes) - c.SwaggerEndpoint("/openapi/2.0.1/openapi.json", "World Analysis API"); + c.SwaggerEndpoint("/openapi/" + apiVersion + "/openapi.json", "World Analysis API"); //TODO: Or alternatively use the original OpenAPI contract that's included in the static files // c.SwaggerEndpoint("/openapi-original.json", "World Analysis API Original"); diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/wwwroot/ws.html b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/wwwroot/ws.html index bf3da4b51bc53cc4b3425f72d4da5860b2b24f25..cbc7959284563fd214e85b696773559cc48d3902 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/wwwroot/ws.html +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/wwwroot/ws.html @@ -71,6 +71,7 @@ var port = document.location.port ? (":" + document.location.port) : ""; connectionUrl.value = scheme + "://" + document.location.hostname + port + "/ws" ; + //connectionUrl.value = scheme + "://" + "etsi.hhi.fraunhofer.de" + "/ws" ; function updateState() { function disable() {