From 88513ff4167c18a3ed2bcd501a3ddb89dc3abc20 Mon Sep 17 00:00:00 2001 From: Sylvain Renault Date: Thu, 8 Aug 2024 18:03:45 +0200 Subject: [PATCH 1/6] Some trst implementation for REST. First websocket functions (register, send poses...) --- .../ControllersImpl/CapabilitiesImpl.cs | 24 ++- .../ETSI-ARF/ControllersImpl/PoseImpl.cs | 9 +- .../ControllersImpl/WebSocketController.cs | 169 ++++++++++++++++-- .../ETSI-ARF/ModelsExt/NotNeeded.txt | 0 .../ETSI-ARF/NoMongoDBneeded.txt | 0 .../ETSI-ARF/Services/NotNeeded.txt | 0 .../ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs | 11 +- .../wwwroot/ws.html | 1 + 8 files changed, 187 insertions(+), 27 deletions(-) create mode 100644 server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/ModelsExt/NotNeeded.txt create mode 100644 server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/NoMongoDBneeded.txt create mode 100644 server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/Services/NotNeeded.txt 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 3529f66..648b36f 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!"); + // Get all capabilities from all anchors/trackables + // test + Capability capability = new Capability(); + capability.Framerate = 24; + capability.Latency = 0; + capability.TrackableType = TrackableType.FIDUCIALMARKEREnum; + + // Create list + List capabilities = new List(); + capabilities.Add(capability); + + // Create repsonse object + GetCapabilities200Response response = new GetCapabilities200Response(); + response.Capabilities = capabilities; + return new ObjectResult(response); + //return StatusCode(405, "Not supported yet!"); } /// @@ -61,7 +76,12 @@ 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!"); + Capability capability = new Capability(); + capability.Framerate = 24; + capability.Latency = 0; + capability.TrackableType = TrackableType.FIDUCIALMARKEREnum; + return (null != capability) ? new ObjectResult(capability) : StatusCode(404, "Not found, could not find capability for UUID: " + trackableOrAnchorUUID); + //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 93f84bf..1fbaa52 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 @@ -85,7 +85,14 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult SubscribeToPose([FromBody]SubscribeToPoseRequest subscribeToPoseRequest, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) { - return StatusCode(405, "Not supported yet!"); + SubscribeToPose200Response response = new SubscribeToPose200Response(); + response.Target = subscribeToPoseRequest.Target; + response.Mode = subscribeToPoseRequest.Mode[0]; + response.WebhookUrl = subscribeToPoseRequest.WebhookUrl; + response.WebsocketUrl = Request.Host.ToString(); ; + + return new ObjectResult(response); + //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 6e80b54..f0c81d3 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 @@ -31,6 +31,8 @@ using Microsoft.AspNetCore.Http; using Swashbuckle.AspNetCore.Annotations; using Swashbuckle.AspNetCore.SwaggerGen; +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 { @@ -40,6 +42,11 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services // public class WebSocketController : ControllerBase { + private string currentName = ""; + private bool registered = false; + private bool firstTime = true; + private int timeCnt = 3; + [HttpGet("/ws")] public async Task Get() { @@ -47,12 +54,14 @@ 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!"); } - await Echo(HttpContext, webSocket); + else if (webSocket.State == WebSocketState.Open) + { + await HandleClientData(HttpContext, webSocket); + } } else { @@ -76,17 +85,22 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services // // Send the time all seconds // + //[HttpGet("/ws/time")] private async Task SendTime(WebSocket webSocket) { 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); + var message = "ARF World Analysis Server, Time = " + DateTime.Now.ToLocalTime(); if (webSocket.State == WebSocketState.Open) { - await webSocket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None); + timeCnt--; + if (timeCnt < 0) + { + await SendText(webSocket, "Stop"); + break; + } + else await SendText(webSocket, message); } else if (webSocket.State == WebSocketState.Closed || webSocket.State == WebSocketState.Aborted) { @@ -96,28 +110,145 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services } } - // - // Echo incoming messages - // - private async Task Echo(HttpContext context, WebSocket webSocket) + float rotInc = 0; + private async Task SendPose(WebSocket webSocket) + { + while (true) + { + if (webSocket.State == WebSocketState.Open) + { + timeCnt--; + if (timeCnt < 0) + { + await SendText(webSocket, "Stop"); + 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(webSocket, json); + } + } + else if (webSocket.State == WebSocketState.Closed || webSocket.State == WebSocketState.Aborted) + { + break; + } + Thread.Sleep(250); + } + } + + private async void OnReceiveText(WebSocket webSocket, string msg) + { + if (firstTime) + { + // Register the client/module + if (msg.ToLower().StartsWith("module:")) + { + registered = true; + firstTime = false; + currentName = msg.Split(':')[1]; + await SendText(webSocket, "ARF World Analysis Server: You are now registered as a module: " + currentName); + } + else if (msg.ToLower().StartsWith("client:")) + { + registered = true; + firstTime = false; + currentName = msg.Split(':')[1]; + await SendText(webSocket, "ARF World Analysis Server: You are now registered as a client: " + currentName); + } + else + { + registered = false; + await SendText(webSocket, "ARF World Analysis Server: Cannot register " + msg); + } + } + else if (registered) + { + if (msg.ToLower().StartsWith("startsendingpose")) + { + if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); + else timeCnt = 3; + await SendPose(webSocket); + } + else if (msg.ToLower().StartsWith("time")) + { + if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); + else timeCnt = 3; + await SendTime(webSocket); + } + else if (msg.ToLower() == "unregister") + { + // Unregister client/user + currentName = ""; + firstTime = true; + registered = false; + await SendText(webSocket, "Stop"); + } + else + { + // Send a response + await SendText(webSocket, "ARF World Analysis Server: I got this unknown message: " + msg); + } + } + } + + private async Task HandleClientData(HttpContext 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) + if (result.MessageType == WebSocketMessageType.Text) { - // 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); + string getMsg = System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); + OnReceiveText(webSocket, getMsg); + } - // get the next block + // Entering the loop + while (!result.CloseStatus.HasValue) + { + // Read/get the next block result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + if (result.MessageType == WebSocketMessageType.Text) + { + string getMsg = System.Text.Encoding.UTF8.GetString(buffer, 0, result.Count); + OnReceiveText(webSocket, getMsg); + } } await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); } + + + // + // Echo incoming messages + // + private async Task Echo(WebSocketReceiveResult context, WebSocket webSocket) + { + var buffer = new byte[1024 * 4]; + + // Read/get the first data block + WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), 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); + } } } #pragma warning restore CS1591 // Fehlendes XML-Kommentar für öffentlich sichtbaren Typ oder Element 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 0000000..e69de29 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 0000000..e69de29 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 0000000..e69de29 diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs index 48be552..f56fad2 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs @@ -161,14 +161,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(); 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 bf3da4b..cbc7959 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() { -- GitLab From 53394218f49fe0425161456d329d17599835626e Mon Sep 17 00:00:00 2001 From: Sylvain Renault Date: Fri, 16 Aug 2024 12:08:42 +0200 Subject: [PATCH 2/6] Client/module registration for websockets. WIP class to manage clients/modules. --- openapi | 1 + .../ETSI.ARF.OpenAPI.WorldAnalysis/Dockerfile | 8 +-- .../ControllersImpl/CapabilitiesImpl.cs | 11 +++- .../ETSI-ARF/ControllersImpl/PoseImpl.cs | 2 +- .../ControllersImpl/WebSocketController.cs | 61 +++++++++++++++---- .../ETSI-ARF/WorldAnalysisModules.cs | 46 ++++++++++++++ 6 files changed, 109 insertions(+), 20 deletions(-) create mode 160000 openapi create mode 100644 server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs diff --git a/openapi b/openapi new file mode 160000 index 0000000..073fd72 --- /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 1203a89..76f44e2 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 648b36f..f5b295d 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 @@ -55,14 +55,21 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers { // Get all capabilities from all anchors/trackables // test + EncodingInformationStructure enc = new EncodingInformationStructure(); + enc.DataFormat = EncodingInformationStructure.DataFormatEnum.OTHEREnum; + enc.VarVersion = "12345"; + Capability capability = new Capability(); + capability.TrackableType = TrackableType.FIDUCIALMARKEREnum; + capability.EncodingInformation = enc; + capability.Accuracy = 1; capability.Framerate = 24; capability.Latency = 0; - capability.TrackableType = TrackableType.FIDUCIALMARKEREnum; // Create list List capabilities = new List(); - capabilities.Add(capability); + //capabilities.Add(capability); + capabilities.AddRange(WorldAnalysisModules.Singleton.GetCapabilities()); // Create repsonse object GetCapabilities200Response response = new GetCapabilities200Response(); 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 1fbaa52..3489e85 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 @@ -88,7 +88,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers SubscribeToPose200Response response = new SubscribeToPose200Response(); response.Target = subscribeToPoseRequest.Target; response.Mode = subscribeToPoseRequest.Mode[0]; - response.WebhookUrl = subscribeToPoseRequest.WebhookUrl; + response.WebhookUrl = ""; response.WebsocketUrl = Request.Host.ToString(); ; return new ObjectResult(response); 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 f0c81d3..1ad1076 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,11 +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 @@ -56,7 +57,7 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services if (webSocket.State == WebSocketState.Connecting) { - // Response an OK message + // Response an OK message? } else if (webSocket.State == WebSocketState.Open) { @@ -97,7 +98,7 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services timeCnt--; if (timeCnt < 0) { - await SendText(webSocket, "Stop"); + await SendText(webSocket, "TimeStop"); break; } else await SendText(webSocket, message); @@ -120,7 +121,7 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services timeCnt--; if (timeCnt < 0) { - await SendText(webSocket, "Stop"); + await SendText(webSocket, "PoseStop"); break; } else @@ -147,30 +148,49 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services } else if (webSocket.State == WebSocketState.Closed || webSocket.State == WebSocketState.Aborted) { + if (WorldAnalysisModules.Singleton.modules.Contains(currentModule)) WorldAnalysisModules.Singleton.modules.Remove(currentModule); + if (WorldAnalysisModules.Singleton.clients.Contains(currentClient)) WorldAnalysisModules.Singleton.clients.Remove(currentClient); + currentClient = null; + currentModule = null; break; } Thread.Sleep(250); } } + private Module currentModule; + private Client currentClient; + private async void OnReceiveText(WebSocket webSocket, string msg) { if (firstTime) { // Register the client/module - if (msg.ToLower().StartsWith("module:")) + if (msg.StartsWith("Module:")) { registered = true; firstTime = false; currentName = msg.Split(':')[1]; - await SendText(webSocket, "ARF World Analysis Server: You are now registered as a module: " + currentName); + if (currentModule == null && currentClient == null) + { + currentModule = new Module(); + currentModule.name = currentName; + WorldAnalysisModules.Singleton.modules.Add(currentModule); + await SendText(webSocket, "ARF World Analysis Server: You are now registered as a module: " + currentName); + } } - else if (msg.ToLower().StartsWith("client:")) + else if (msg.StartsWith("Client:")) { registered = true; firstTime = false; currentName = msg.Split(':')[1]; - await SendText(webSocket, "ARF World Analysis Server: You are now registered as a client: " + currentName); + if (currentModule == null && currentClient == null) + { + currentClient = new Client(); + currentClient.name = currentName; + WorldAnalysisModules.Singleton.clients.Add(currentClient); + await SendText(webSocket, "ARF World Analysis Server: You are now registered as a client: " + currentName); + } } else { @@ -180,25 +200,40 @@ namespace ETSI.ARF.OpenAPI.WorldStorage.Services } else if (registered) { - if (msg.ToLower().StartsWith("startsendingpose")) + if (msg.StartsWith("StartSendingPose")) { if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); else timeCnt = 3; await SendPose(webSocket); - } - else if (msg.ToLower().StartsWith("time")) + } + else if (msg.StartsWith("Time")) { if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); else timeCnt = 3; await SendTime(webSocket); } - else if (msg.ToLower() == "unregister") + else if (msg.StartsWith("Capabilities")) + { + // Client is sending its capabilities + string[] str_cap = msg.Split('='); + Capability _c = JsonConvert.DeserializeObject(str_cap[1]); + if (currentModule != null) currentModule.capabilities.Add(_c); + } + else if (msg == "UnregisterClient") { // Unregister client/user currentName = ""; firstTime = true; registered = false; - await SendText(webSocket, "Stop"); + await SendText(webSocket, "SessionStop"); + } + else if (msg == "Idle") + { + await SendText(webSocket, "Idle"); + } + else if (msg == "Busy") + { + await SendText(webSocket, "Busy"); } else { diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs new file mode 100644 index 0000000..951472f --- /dev/null +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs @@ -0,0 +1,46 @@ +using ETSI.ARF.OpenAPI.WorldAnalysis.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +#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(); + } + + // For management of WA clinet (e.g. Unity client) + public class Client + { + public string name; + public string session; + + } + + public class WorldAnalysisModules + { + static public WorldAnalysisModules Singleton = new WorldAnalysisModules(); + + public List modules = new List(); + public List clients = new List(); + + // 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; + } + } +} +#pragma warning restore CS1591 // Fehlendes XML-Kommentar für öffentlich sichtbaren Typ oder Element -- GitLab From 13f4319d956cd32b4d55e6014844a606c8cc884d Mon Sep 17 00:00:00 2001 From: Sylvain Renault Date: Thu, 22 Aug 2024 18:38:04 +0200 Subject: [PATCH 3/6] Draft implementation of the websockets logic --- .../ControllersImpl/CapabilitiesImpl.cs | 37 +++++------ .../ETSI-ARF/ControllersImpl/PoseImpl.cs | 65 +++++++++++++++++-- .../ControllersImpl/WebSocketController.cs | 8 +-- .../ETSI-ARF/WorldAnalysisModules.cs | 13 ++++ .../ETSI.ARF.OpenAPI.WorldAnalysis/Program.cs | 2 +- 5 files changed, 94 insertions(+), 31 deletions(-) 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 f5b295d..f9c1a35 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,27 +53,17 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult GetCapabilities([FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) { - // Get all capabilities from all anchors/trackables - // test - EncodingInformationStructure enc = new EncodingInformationStructure(); - enc.DataFormat = EncodingInformationStructure.DataFormatEnum.OTHEREnum; - enc.VarVersion = "12345"; + // todo: compare token and sessionID - Capability capability = new Capability(); - capability.TrackableType = TrackableType.FIDUCIALMARKEREnum; - capability.EncodingInformation = enc; - capability.Accuracy = 1; - capability.Framerate = 24; - capability.Latency = 0; + // Get all capabilities from all anchors/trackables // Create list - List capabilities = new List(); - //capabilities.Add(capability); - capabilities.AddRange(WorldAnalysisModules.Singleton.GetCapabilities()); + List capabilitiesList = new List(); + capabilitiesList.AddRange(WorldAnalysisModules.Singleton.GetCapabilities()); - // Create repsonse object + // Create response object GetCapabilities200Response response = new GetCapabilities200Response(); - response.Capabilities = capabilities; + response.Capabilities = capabilitiesList; return new ObjectResult(response); //return StatusCode(405, "Not supported yet!"); } @@ -83,11 +73,16 @@ 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) { - Capability capability = new Capability(); - capability.Framerate = 24; - capability.Latency = 0; - capability.TrackableType = TrackableType.FIDUCIALMARKEREnum; - return (null != capability) ? new ObjectResult(capability) : StatusCode(404, "Not found, could not find capability for UUID: " + trackableOrAnchorUUID); + // todo: compare token and sessionID + + // Create list + List capabilitiesList = new List(); + capabilitiesList.AddRange(WorldAnalysisModules.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 3489e85..2efde95 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 @@ -53,6 +53,8 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult ConfigureFramerate([FromBody]PoseConfiguration poseConfiguration, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) { + // todo: compare token and sessionID + return StatusCode(405, "Not supported yet!"); } @@ -61,6 +63,8 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// 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) { + // todo: compare token and sessionID + return StatusCode(405, "Not supported yet!"); } @@ -72,11 +76,33 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers return StatusCode(405, "Not supported yet!"); } +#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 uuid; // id of subscription (id is defined by the WA server) + public Guid uuidTarget; // id trackable or anchor + public DateTime timeValidity; //The duration of the validity of the subscription + public Pose pose; + //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) { + // todo: compare token and sessionID + return StatusCode(405, "Not supported yet!"); } @@ -85,12 +111,37 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult SubscribeToPose([FromBody]SubscribeToPoseRequest subscribeToPoseRequest, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) { + // todo: compare token and sessionID + + int validity = subscribeToPoseRequest.Validity; // todo: is to handle here or by the client? + + // We add the subscription + SubscriptionInfo sub = new SubscriptionInfo(); + sub.uuid = new Guid(); + sub.timeValidity = DateTime.Now.AddMilliseconds(validity / 1000.0f); + sub.pose = new Pose(); + sub.pose.Mode = subscribeToPoseRequest.Mode[0]; + sub.uuidTarget = subscribeToPoseRequest.Target; + m_subscriptionsPoses.Add(sub.uuid, sub); + SubscribeToPose200Response response = new SubscribeToPose200Response(); - response.Target = subscribeToPoseRequest.Target; - response.Mode = subscribeToPoseRequest.Mode[0]; - response.WebhookUrl = ""; - response.WebsocketUrl = Request.Host.ToString(); ; - + response.Uuid = sub.uuid; + response.Validity = validity; + response.Target = sub.uuidTarget; + response.Mode = sub.pose.Mode; + if (string.IsNullOrEmpty(subscribeToPoseRequest.WebhookUrl)) + { + response.WebhookUrl = ""; + response.WebsocketUrl = Request.Host.ToString(); + // Notice: starting websocket server is done autom. by the client, when calling "URL:/ws" + + // todo: register the client, so the websocket will send to it pose updates + } + else + { + response.WebhookUrl = subscribeToPoseRequest.WebhookUrl; + response.WebsocketUrl = null; + } return new ObjectResult(response); //return StatusCode(405, "Not supported yet!"); } @@ -100,6 +151,8 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult UnsubscribeFromPose([FromRoute (Name = "subscriptionUUID")][Required]Guid subscriptionUUID, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) { + // todo: compare token and sessionID + return StatusCode(405, "Not supported yet!"); } @@ -108,6 +161,8 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult UpdateSubscription([FromRoute (Name = "subscriptionUUID")][Required]Guid subscriptionUUID, [FromBody]UpdateSubscriptionRequest updateSubscriptionRequest, [FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) { + // todo: compare token and sessionID + 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 1ad1076..0722715 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 @@ -166,7 +166,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers if (firstTime) { // Register the client/module - if (msg.StartsWith("Module:")) + if (msg.StartsWith("RegisterModule:")) { registered = true; firstTime = false; @@ -179,7 +179,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers await SendText(webSocket, "ARF World Analysis Server: You are now registered as a module: " + currentName); } } - else if (msg.StartsWith("Client:")) + else if (msg.StartsWith("RegisterClient:")) { registered = true; firstTime = false; @@ -200,13 +200,13 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers } else if (registered) { - if (msg.StartsWith("StartSendingPose")) + if (msg.StartsWith("PoseStart")) { if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); else timeCnt = 3; await SendPose(webSocket); } - else if (msg.StartsWith("Time")) + else if (msg.StartsWith("TimeStart")) { if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); else timeCnt = 3; diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs index 951472f..f4b3938 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs @@ -41,6 +41,19 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis } 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 storage via the module? + + list.AddRange(item.capabilities); + } + return list; + } } } #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 95e2206..1fba731 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!!! }); } } -- GitLab From afbfe86d0a75ca3fa70a80b9d364ccf4aaf3d8f7 Mon Sep 17 00:00:00 2001 From: Sylvain Renault Date: Thu, 5 Sep 2024 13:31:02 +0200 Subject: [PATCH 4/6] Implementation of the new API (generator issues) --- openapi | 2 +- .../ETSI-ARF/ControllersImpl/PoseImpl.cs | 81 +++++++++++++------ .../ControllersImpl/WebSocketController.cs | 72 +++++++++++++---- .../ETSI-ARF/WorldAnalysisModules.cs | 60 +++++++++++++- 4 files changed, 169 insertions(+), 46 deletions(-) diff --git a/openapi b/openapi index 073fd72..b639a02 160000 --- a/openapi +++ b/openapi @@ -1 +1 @@ -Subproject commit 073fd7213fd9e6ebc2f8a47d628a650de30c8bc4 +Subproject commit b639a02180c2b5e301c77483b3a2fa645ba94169 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 2efde95..058e388 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,39 +41,52 @@ 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) { // todo: compare token and sessionID - return StatusCode(405, "Not supported yet!"); + // Notify the modules that the client need a new framerate + WorldAnalysisModules.Singleton.ConfigureFramerate(poseConfiguration); + return StatusCode(200, "Ok."); } /// /// 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) { // todo: compare token and sessionID - return StatusCode(405, "Not supported yet!"); + // Request from the modules a new pose of a single UUID + Pose result; + result = WorldAnalysisModules.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!"); + // todo: compare token and sessionID + + // Request from the modules new poses from all UUIDs + GetPoses200Response result = new GetPoses200Response(); + foreach (var item in uuid) + { + result.Poses.Add(WorldAnalysisModules.Singleton.GetPose(item.Uuid, item.Mode)); + } + return new ObjectResult(result); } #pragma warning disable CS1591 // Fehlendes XML-Kommentar für öffentlich sichtbaren Typ oder Element @@ -99,42 +112,53 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// /// 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) { // todo: compare token and sessionID + // todo: search for a subscription and send it back + return StatusCode(405, "Not supported yet!"); } /// /// 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) { // todo: compare token and sessionID int validity = subscribeToPoseRequest.Validity; // todo: is to handle here or by the client? // We add the subscription - SubscriptionInfo sub = new SubscriptionInfo(); - sub.uuid = new Guid(); - sub.timeValidity = DateTime.Now.AddMilliseconds(validity / 1000.0f); - sub.pose = new Pose(); - sub.pose.Mode = subscribeToPoseRequest.Mode[0]; - sub.uuidTarget = subscribeToPoseRequest.Target; - m_subscriptionsPoses.Add(sub.uuid, sub); + SubscriptionInfo info = new SubscriptionInfo(); + info.uuid = new Guid(); + info.uuidTarget = subscribeToPoseRequest.Target; + info.timeValidity = DateTime.Now.AddMilliseconds(validity / 1000.0f); + info.pose = new Pose(); + info.pose.Mode = subscribeToPoseRequest.Mode; + m_subscriptionsPoses.Add(info.uuid, info); + + // todo: handle multiple subscriptions ? + // subscribeToPoseRequest.Targets[] + // subscribeToPoseRequest.Modes[] + + // 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?!? + WorldAnalysisModules.Singleton.SubscribeToPose(info); SubscribeToPose200Response response = new SubscribeToPose200Response(); - response.Uuid = sub.uuid; response.Validity = validity; - response.Target = sub.uuidTarget; - response.Mode = sub.pose.Mode; + response.Uuid = info.uuid; + response.Target = info.uuidTarget; + response.Mode = info.pose.Mode; + if (string.IsNullOrEmpty(subscribeToPoseRequest.WebhookUrl)) { response.WebhookUrl = ""; response.WebsocketUrl = Request.Host.ToString(); // Notice: starting websocket server is done autom. by the client, when calling "URL:/ws" - + // todo: register the client, so the websocket will send to it pose updates } else @@ -143,26 +167,33 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers response.WebsocketUrl = null; } return new ObjectResult(response); - //return StatusCode(405, "Not supported yet!"); } /// /// 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) { // todo: compare token and sessionID - return StatusCode(405, "Not supported yet!"); + // Remove the subscription from the list? + if (m_subscriptionsPoses.ContainsKey(subscriptionUUID)) m_subscriptionsPoses.Remove(subscriptionUUID); + + // Inform the module(s) that the subscription ended + WorldAnalysisModules.Singleton.UnsubscribeFromPose(subscriptionUUID); + + return StatusCode(200, "Ok!"); } /// /// 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) { // todo: compare token and 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 0722715..8a76568 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 @@ -47,6 +47,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers private bool registered = false; private bool firstTime = true; private int timeCnt = 3; + private WebSocket websockets; [HttpGet("/ws")] public async Task Get() @@ -83,6 +84,15 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers await webSocket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None); } + public void SendText(string text) + { + // Response an OK message + var message = text; + var bytes = Encoding.UTF8.GetBytes(message); + var arraySegment = new ArraySegment(bytes, 0, bytes.Length); + websockets.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None); + } + // // Send the time all seconds // @@ -91,7 +101,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers { while (true) { - var message = "ARF World Analysis Server, Time = " + DateTime.Now.ToLocalTime(); + var message = "Time=" + DateTime.Now.ToLocalTime(); if (webSocket.State == WebSocketState.Open) { @@ -112,7 +122,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers } float rotInc = 0; - private async Task SendPose(WebSocket webSocket) + private async Task SendDemoPose(WebSocket webSocket) { while (true) { @@ -143,7 +153,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers string json = pose.ToJson(); - await SendText(webSocket, json); + await SendText(webSocket, "Pose=" + json); } } else if (webSocket.State == WebSocketState.Closed || webSocket.State == WebSocketState.Aborted) @@ -165,7 +175,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers { if (firstTime) { - // Register the client/module + #region Register the client/module if (msg.StartsWith("RegisterModule:")) { registered = true; @@ -175,6 +185,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers { currentModule = new Module(); currentModule.name = currentName; + currentModule.websockets = this; WorldAnalysisModules.Singleton.modules.Add(currentModule); await SendText(webSocket, "ARF World Analysis Server: You are now registered as a module: " + currentName); } @@ -188,6 +199,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers { currentClient = new Client(); currentClient.name = currentName; + currentClient.websockets = this; WorldAnalysisModules.Singleton.clients.Add(currentClient); await SendText(webSocket, "ARF World Analysis Server: You are now registered as a client: " + currentName); } @@ -197,36 +209,61 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers registered = false; await SendText(webSocket, "ARF World Analysis Server: Cannot register " + msg); } + return; + #endregion } - else if (registered) + + if (registered) { - if (msg.StartsWith("PoseStart")) + // + // Messages from Analysis modules + // + if (msg.StartsWith("PoseNew:")) { - if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); - else timeCnt = 3; - await SendPose(webSocket); - } - else if (msg.StartsWith("TimeStart")) - { - if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); - else timeCnt = 3; - await SendTime(webSocket); + // todo: parse the json string + + // Send the pose to the client(s) + Pose p = new Pose(); + WorldAnalysisModules.Singleton.SendPoseToClients(p); + await SendText(webSocket, "RequestNextPose"); } - else if (msg.StartsWith("Capabilities")) + else if (msg.StartsWith("Capabilities of")) // Receive capab. from a module { // Client is sending its capabilities string[] str_cap = msg.Split('='); Capability _c = JsonConvert.DeserializeObject(str_cap[1]); if (currentModule != null) currentModule.capabilities.Add(_c); } + + // + // Messages from Unity clients + // + else if (msg.StartsWith("PoseStart")) // send some fake poses to Unity clients + { + await SendDemoPose(webSocket); + } + + // + // Messages from modules and clients + // + else if (msg.StartsWith("TimeStart")) // send time to modules/clients + { + if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); + else timeCnt = 3; + await SendTime(webSocket); + } else if (msg == "UnregisterClient") { - // Unregister client/user + // Unregister a client (e.g. Unity client) currentName = ""; firstTime = true; registered = false; await SendText(webSocket, "SessionStop"); } + else if (msg == "PoseIsRegistered") + { + await SendText(webSocket, "RequestNextPose"); + } else if (msg == "Idle") { await SendText(webSocket, "Idle"); @@ -246,6 +283,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers private async Task HandleClientData(HttpContext context, WebSocket webSocket) { var buffer = new byte[1024 * 4]; + websockets = webSocket; // Read/get the first data block WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs index f4b3938..6ec1f56 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs @@ -3,6 +3,8 @@ 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 @@ -12,6 +14,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis { public string name; public List capabilities = new List(); + public WebSocketController websockets; } // For management of WA clinet (e.g. Unity client) @@ -19,7 +22,7 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis { public string name; public string session; - + public WebSocketController websockets; } public class WorldAnalysisModules @@ -48,12 +51,63 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis foreach (var item in modules) { // todo: Check if uuid has the capability? - // Get the world object from the storage via the module? - + // Get the world object from the world storage via the module? + // Use GetRelocalisation() ? list.AddRange(item.capabilities); } return list; } + + public void ConfigureFramerate(PoseConfiguration poseConfiguration) + { + foreach (var item in modules) + { + // todo: configure the module via websocket + item.websockets.SendText("ConfigureFramerate:" + poseConfiguration.ToJson()); + } + } + + 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) + { + // Send to all modules a request of subscription + foreach (var item in modules) + { + item.websockets.SendText("SubscribePose:" + info.uuidTarget.ToString()); + } + } + + public void UnsubscribeFromPose(Guid uuid) + { + // Send to all modules a request of subscription + foreach (var item in modules) + { + item.websockets.SendText("UnsubscribePose:" + uuid.ToString()); + } + } + + public void SendPoseToClients(Pose p) + { + // Send to all clients with valid subscription the new pose + foreach (var item in clients) + { + item.websockets.SendText("Pose=" + p.ToJson()); + } + } } } #pragma warning restore CS1591 // Fehlendes XML-Kommentar für öffentlich sichtbaren Typ oder Element -- GitLab From 42f62762b27910762444711e14e81d8a37f50851 Mon Sep 17 00:00:00 2001 From: Sylvain Renault Date: Tue, 24 Sep 2024 21:17:12 +0200 Subject: [PATCH 5/6] Functions for the API 2.0.0. Complete handling of websocket requests, for modules and clients side. --- openapi | 2 +- .../ControllersImpl/CapabilitiesImpl.cs | 13 +- .../ETSI-ARF/ControllersImpl/PoseImpl.cs | 143 +++++++---- .../ControllersImpl/WebSocketController.cs | 224 ++++++++++-------- ...Modules.cs => WorldAnalysisConnections.cs} | 55 +++-- .../ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs | 31 ++- 6 files changed, 297 insertions(+), 171 deletions(-) rename server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/{WorldAnalysisModules.cs => WorldAnalysisConnections.cs} (57%) diff --git a/openapi b/openapi index b639a02..073fd72 160000 --- a/openapi +++ b/openapi @@ -1 +1 @@ -Subproject commit b639a02180c2b5e301c77483b3a2fa645ba94169 +Subproject commit 073fd7213fd9e6ebc2f8a47d628a650de30c8bc4 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 f9c1a35..4638d5c 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,13 +53,16 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult GetCapabilities([FromHeader (Name = "token")]string token, [FromHeader (Name = "sessionID")]string sessionID) { - // todo: compare token and sessionID + 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(WorldAnalysisModules.Singleton.GetCapabilities()); + capabilitiesList.AddRange(WorldAnalysisConnections.Singleton.GetCapabilities()); // Create response object GetCapabilities200Response response = new GetCapabilities200Response(); @@ -73,11 +76,13 @@ 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) { - // todo: compare token and sessionID + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); + + // todo: compare sessionID // Create list List capabilitiesList = new List(); - capabilitiesList.AddRange(WorldAnalysisModules.Singleton.GetCapabilitiesFromUuid(trackableOrAnchorUUID)); + capabilitiesList.AddRange(WorldAnalysisConnections.Singleton.GetCapabilitiesFromUuid(trackableOrAnchorUUID)); // Create response object GetSupport200Response response = new GetSupport200Response(); 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 058e388..4460641 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 @@ -53,11 +53,13 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult ConfigureFramerate([FromBody] PoseConfiguration poseConfiguration, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - // todo: compare token and sessionID + 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 - WorldAnalysisModules.Singleton.ConfigureFramerate(poseConfiguration); - return StatusCode(200, "Ok."); + bool result = WorldAnalysisConnections.Singleton.ConfigureFramerate(poseConfiguration); + return result ? StatusCode(200, "Ok.") : StatusCode(405, "Not supported."); } /// @@ -65,11 +67,13 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// 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) { - // todo: compare token and sessionID + 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 = WorldAnalysisModules.Singleton.GetPose(trackableOrAnchorUUID, mode); + result = WorldAnalysisConnections.Singleton.GetPose(trackableOrAnchorUUID, mode); return new ObjectResult(result); } @@ -78,13 +82,15 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult GetPoses([FromQuery(Name = "uuid")][Required()] List uuid, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - // todo: compare token and sessionID + 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(WorldAnalysisModules.Singleton.GetPose(item.Uuid, item.Mode)); + result.Poses.Add(WorldAnalysisConnections.Singleton.GetPose(item.Uuid, item.Mode)); } return new ObjectResult(result); } @@ -101,10 +107,13 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers public struct SubscriptionInfo { - public Guid uuid; // id of subscription (id is defined by the WA server) - public Guid uuidTarget; // id trackable or anchor - public DateTime timeValidity; //The duration of the validity of the subscription + 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 @@ -114,11 +123,26 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult GetSubscription([FromRoute(Name = "subscriptionUUID")][Required] Guid subscriptionUUID, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - // todo: compare token and sessionID + if (!Startup.IsAccessGranted(token)) return StatusCode(511, new Error() { Message = "Invalid token!" }); - // todo: search for a subscription and send it back + // todo: compare sessionID - return StatusCode(405, "Not supported yet!"); + // 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."); } /// @@ -126,46 +150,67 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult SubscribeToPose([FromBody] SubscribeToPoseRequest subscribeToPoseRequest, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - // todo: compare token and sessionID + // 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.uuid = new Guid(); - info.uuidTarget = subscribeToPoseRequest.Target; + info.uuidSub = response.Uuid; + info.webSocket = response.WebsocketUrl; info.timeValidity = DateTime.Now.AddMilliseconds(validity / 1000.0f); + info.pose = new Pose(); - info.pose.Mode = subscribeToPoseRequest.Mode; - m_subscriptionsPoses.Add(info.uuid, info); + info.pose.Mode = response.Mode; - // todo: handle multiple subscriptions ? - // subscribeToPoseRequest.Targets[] - // subscribeToPoseRequest.Modes[] + 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?!? - WorldAnalysisModules.Singleton.SubscribeToPose(info); - - SubscribeToPose200Response response = new SubscribeToPose200Response(); - response.Validity = validity; - response.Uuid = info.uuid; - response.Target = info.uuidTarget; - response.Mode = info.pose.Mode; - - if (string.IsNullOrEmpty(subscribeToPoseRequest.WebhookUrl)) - { - response.WebhookUrl = ""; - response.WebsocketUrl = Request.Host.ToString(); - // Notice: starting websocket server is done autom. by the client, when calling "URL:/ws" + WorldAnalysisConnections.Singleton.SubscribeToPose(info, parentUUID); - // todo: register the client, so the websocket will send to it pose updates - } - else - { - response.WebhookUrl = subscribeToPoseRequest.WebhookUrl; - response.WebsocketUrl = null; - } return new ObjectResult(response); } @@ -174,15 +219,21 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult UnsubscribeFromPose([FromRoute(Name = "subscriptionUUID")][Required] Guid subscriptionUUID, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - // todo: compare token and sessionID + 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 - WorldAnalysisModules.Singleton.UnsubscribeFromPose(subscriptionUUID); + WorldAnalysisConnections.Singleton.UnsubscribeFromPose(subscriptionUUID); - return StatusCode(200, "Ok!"); + return StatusCode(200, new Success() { Message = "Unsubscription successfull." }); } /// @@ -190,7 +241,9 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers /// public override IActionResult UpdateSubscription([FromRoute(Name = "subscriptionUUID")][Required] Guid subscriptionUUID, [FromBody] UpdateSubscriptionRequest updateSubscriptionRequest, [FromHeader(Name = "token")] string token, [FromHeader(Name = "sessionID")] string sessionID) { - // todo: compare token and 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 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 8a76568..8229311 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 @@ -43,11 +43,13 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers // 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; - private WebSocket websockets; [HttpGet("/ws")] public async Task Get() @@ -62,7 +64,8 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers } else if (webSocket.State == WebSocketState.Open) { - await HandleClientData(HttpContext, webSocket); + // Go to the loop... + await WebSocketServer_Loop(HttpContext, webSocket); } } else @@ -75,63 +78,36 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers // // 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 text) + public void SendText(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); - websockets.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None); + websocket.SendAsync(arraySegment, WebSocketMessageType.Text, true, CancellationToken.None); } // - // Send the time all seconds + // Send a demo pose all seconds // - //[HttpGet("/ws/time")] - private async Task SendTime(WebSocket webSocket) - { - while (true) - { - var message = "Time=" + DateTime.Now.ToLocalTime(); - - if (webSocket.State == WebSocketState.Open) - { - timeCnt--; - if (timeCnt < 0) - { - await SendText(webSocket, "TimeStop"); - break; - } - else await SendText(webSocket, message); - } - else if (webSocket.State == WebSocketState.Closed || webSocket.State == WebSocketState.Aborted) - { - break; - } - Thread.Sleep(1000); - } - } - float rotInc = 0; - private async Task SendDemoPose(WebSocket webSocket) + private async Task SendDemoPose(WebSocket ws) { while (true) { - if (webSocket.State == WebSocketState.Open) + if (ws.State == WebSocketState.Open) { timeCnt--; if (timeCnt < 0) { - await SendText(webSocket, "PoseStop"); + await SendText(ws, "PoseStop"); break; } else @@ -153,13 +129,13 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers string json = pose.ToJson(); - await SendText(webSocket, "Pose=" + json); + 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 (WorldAnalysisModules.Singleton.modules.Contains(currentModule)) WorldAnalysisModules.Singleton.modules.Remove(currentModule); - if (WorldAnalysisModules.Singleton.clients.Contains(currentClient)) WorldAnalysisModules.Singleton.clients.Remove(currentClient); + 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; @@ -171,140 +147,196 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers private Module currentModule; private Client currentClient; - private async void OnReceiveText(WebSocket webSocket, string msg) + private async void OnReceiveText(string msg) { + #region Register the client/module if (firstTime) { - #region Register the client/module - if (msg.StartsWith("RegisterModule:")) + if (msg.StartsWith("RegisterModule=")) { registered = true; firstTime = false; - currentName = msg.Split(':')[1]; + currentName = msg.Split('=')[1]; if (currentModule == null && currentClient == null) { currentModule = new Module(); currentModule.name = currentName; currentModule.websockets = this; - WorldAnalysisModules.Singleton.modules.Add(currentModule); - await SendText(webSocket, "ARF World Analysis Server: You are now registered as a module: " + currentName); + WorldAnalysisConnections.Singleton.modules.Add(currentModule); + SendText($"ARF World Analysis Server: #{ WebSocketControllerInstanceCount } You are now registered as module: { currentName }"); } } - else if (msg.StartsWith("RegisterClient:")) + else if (msg.StartsWith("RegisterClient=")) { registered = true; firstTime = false; - currentName = msg.Split(':')[1]; + currentName = msg.Split('=')[1]; if (currentModule == null && currentClient == null) { currentClient = new Client(); currentClient.name = currentName; currentClient.websockets = this; - WorldAnalysisModules.Singleton.clients.Add(currentClient); - await SendText(webSocket, "ARF World Analysis Server: You are now registered as a client: " + currentName); + WorldAnalysisConnections.Singleton.clients.Add(currentClient); + SendText($"ARF World Analysis Server: #{ WebSocketControllerInstanceCount } You are now registered as client: { currentName }"); } } else { registered = false; - await SendText(webSocket, "ARF World Analysis Server: Cannot register " + msg); + SendText("ARF World Analysis Server: Cannot register " + msg); } return; - #endregion } + #endregion if (registered) { // - // Messages from Analysis modules + // Some admin stuffs // - if (msg.StartsWith("PoseNew:")) + if (msg == "Idle") { - // todo: parse the json string - - // Send the pose to the client(s) - Pose p = new Pose(); - WorldAnalysisModules.Singleton.SendPoseToClients(p); - await SendText(webSocket, "RequestNextPose"); + SendText("Idle"); } - else if (msg.StartsWith("Capabilities of")) // Receive capab. from a module + else if (msg == "Busy") { - // Client is sending its capabilities - string[] str_cap = msg.Split('='); - Capability _c = JsonConvert.DeserializeObject(str_cap[1]); - if (currentModule != null) currentModule.capabilities.Add(_c); + 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; + } // - // Messages from Unity clients + // OpenAPI // - else if (msg.StartsWith("PoseStart")) // send some fake poses to Unity clients + else if (msg == "PoseIsNowSubscribed") { - await SendDemoPose(webSocket); + SendText("RequestNextPose"); } // - // Messages from modules and clients + // Messages from a module (Analysis) // - else if (msg.StartsWith("TimeStart")) // send time to modules/clients + else if (msg.StartsWith("Capabilities=")) // Receive capab. from a module { - if (msg.Contains(':')) timeCnt = int.Parse(msg.Split(':')[1]); - else timeCnt = 3; - await SendTime(webSocket); + // Module is sending their capabilities + string[] str_cap = msg.Split('='); + Capability _c = JsonConvert.DeserializeObject(str_cap[1]); + if (currentModule != null) currentModule.capabilities.Add(_c); } - else if (msg == "UnregisterClient") + else if (msg.StartsWith("NewPose=")) { - // Unregister a client (e.g. Unity client) - currentName = ""; - firstTime = true; - registered = false; - await SendText(webSocket, "SessionStop"); + 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 == "PoseIsRegistered") + else if (msg.StartsWith("Error=")) { - await SendText(webSocket, "RequestNextPose"); + string errorMsg = msg.Split('=')[1]; + foreach (var item in WorldAnalysisConnections.Singleton.clients) + { + await SendText(item.websockets.websocket, errorMsg); + } } - else if (msg == "Idle") + + // + // Messages from a client (Unity) + // + else if (msg.StartsWith("PoseStart")) // send some fake poses to Unity clients { - await SendText(webSocket, "Idle"); + await SendDemoPose(websocket); } - else if (msg == "Busy") + else if (msg.StartsWith("CurrentUserPose=")) { - await SendText(webSocket, "Busy"); + // 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 - await SendText(webSocket, "ARF World Analysis Server: I got this unknown message: " + msg); + SendText("ARF World Analysis Server: I got this unknown message: " + msg); } } } - private async Task HandleClientData(HttpContext context, WebSocket webSocket) + private async Task WebSocketServer_Loop(HttpContext context, WebSocket ws) { + websocket = ws; + WebSocketControllerInstanceCount++; + var buffer = new byte[1024 * 4]; - websockets = webSocket; // Read/get the first data block - WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + 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(webSocket, getMsg); + OnReceiveText(getMsg); } // Entering the loop while (!result.CloseStatus.HasValue) { // Read/get the next block - result = await webSocket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + 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(webSocket, getMsg); + OnReceiveText(getMsg); } } - await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + await ws.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None); + WebSocketControllerInstanceCount--; } diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisConnections.cs similarity index 57% rename from server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs rename to server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisConnections.cs index 6ec1f56..c864547 100644 --- a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisModules.cs +++ b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/ETSI-ARF/WorldAnalysisConnections.cs @@ -17,20 +17,21 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis public WebSocketController websockets; } - // For management of WA clinet (e.g. Unity client) + // 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 WorldAnalysisModules + public class WorldAnalysisConnections { - static public WorldAnalysisModules Singleton = new WorldAnalysisModules(); + static public WorldAnalysisConnections Singleton = new WorldAnalysisConnections(); public List modules = new List(); - public List clients = new List(); + public List clients = new List(); // e.g. Unity clients // todo // Manage the sessions and modules @@ -58,13 +59,24 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis return list; } - public void ConfigureFramerate(PoseConfiguration poseConfiguration) + public bool ConfigureFramerate(PoseConfiguration poseConfiguration) { - foreach (var item in modules) + bool ok = false; + foreach (var m in modules) { - // todo: configure the module via websocket - item.websockets.SendText("ConfigureFramerate:" + poseConfiguration.ToJson()); + 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) @@ -73,39 +85,48 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis foreach (var item in modules) { // todo: get the pose via websocket - item.websockets.SendText("GetPose:" + trackableOrAnchorUUID + ":" + mode.ToString()); + 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) + + public void SubscribeToPose(SubscriptionInfo info, Guid parentTrackableUUID) { // Send to all modules a request of subscription foreach (var item in modules) { - item.websockets.SendText("SubscribePose:" + info.uuidTarget.ToString()); + 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 uuid) + public void UnsubscribeFromPose(Guid subscriptionUUID) { // Send to all modules a request of subscription foreach (var item in modules) { - item.websockets.SendText("UnsubscribePose:" + uuid.ToString()); + 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(Pose p) + public void SendPoseToClients(string str_pose) { // Send to all clients with valid subscription the new pose foreach (var item in clients) { - item.websockets.SendText("Pose=" + p.ToJson()); + item.websockets.SendText("NewPose=" + str_pose); } } } diff --git a/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs b/server/worldanalysis/src/ETSI.ARF.OpenAPI.WorldAnalysis/Startup.cs index f56fad2..2a3f67c 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"); @@ -183,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"); -- GitLab From 571913d7a37dfa8124f9193024b04c67185c0e91 Mon Sep 17 00:00:00 2001 From: Sylvain Renault Date: Wed, 25 Sep 2024 16:21:54 +0200 Subject: [PATCH 6/6] Registration of modules optimized if same modules register. --- .../ControllersImpl/WebSocketController.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) 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 8229311..4731534 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 @@ -157,14 +157,26 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers 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 }"); } + SendText($"ARF World Analysis Server: #{ WebSocketControllerInstanceCount } You are now registered as module: { currentName }"); } else if (msg.StartsWith("RegisterClient=")) { @@ -249,8 +261,13 @@ namespace ETSI.ARF.OpenAPI.WorldAnalysis.Controllers { // Module is sending their capabilities string[] str_cap = msg.Split('='); - Capability _c = JsonConvert.DeserializeObject(str_cap[1]); - if (currentModule != null) currentModule.capabilities.Add(_c); + 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=")) { -- GitLab