diff --git a/.github/workflows/backend_lint_and_test.yml b/.github/workflows/backend_lint_and_test.yml index 53d2209b9..969d245fd 100644 --- a/.github/workflows/backend_lint_and_test.yml +++ b/.github/workflows/backend_lint_and_test.yml @@ -53,14 +53,7 @@ jobs: with: dotnet-version: "8.0.x" - # Dotnet format is included in the .NET8 SDK - # By default, the task ensures the exit code is 0 - # If a file needs to be edited by dotnet format, the exit code will be a non-zero value - # We are using severity level 'info' here. - - name: Run dotnet format - run: dotnet format --severity info --verbosity diagnostic --verify-no-changes --exclude ./api/migrations - - name: Run csharpier format run: | dotnet tool restore - dotnet csharpier --check + dotnet csharpier . --check diff --git a/backend/.csharpierignore b/backend/.csharpierignore new file mode 100644 index 000000000..4ae0ea625 --- /dev/null +++ b/backend/.csharpierignore @@ -0,0 +1 @@ +**/Migrations/*.cs diff --git a/backend/README.md b/backend/README.md index c443f4d1e..daf66e1a8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -257,30 +257,10 @@ psql U Username -d postgres -h host_name_or_adress -p port -f ouput_file_name.sl ### CSharpier -In everyday development we use [CSharpier](https://csharpier.com/) to auto-format code on save. Installation procedure is described [here](https://csharpier.com/docs/About). No configuration should be required. - -### Dotnet format - The formatting of the backend is defined in the [.editorconfig file](../.editorconfig). -We use [dotnet format](https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-format) -to format and verify code style in backend based on the -[C# coding conventions](https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions). - -Dotnet format is included in the .NET SDK. - -To check the formatting, run the following command in the backend folder: - -``` -cd backend -dotnet format --severity info --verbosity diagnostic --verify-no-changes --exclude ./api/migrations -``` - -dotnet format is used to detect naming conventions and other code-related issues. They can be fixed by - -``` -dotnet format --severity info -``` +In everyday development we use [CSharpier](https://csharpier.com/) to auto-format code on save. Installation procedure is described [here](https://csharpier.com/docs/About). No configuration should be required. To run csharpier locally, go to the backend folder and run: +`dotnet csharpier . --check` ## SignalR diff --git a/backend/api.test/Client/AreaTests.cs b/backend/api.test/Client/AreaTests.cs index 190a86a9b..4ced9336a 100644 --- a/backend/api.test/Client/AreaTests.cs +++ b/backend/api.test/Client/AreaTests.cs @@ -13,6 +13,7 @@ using Api.Test.Database; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; + namespace Api.Test.Client { [Collection("Database collection")] @@ -23,27 +24,27 @@ public class AreaTests : IClassFixture> private readonly JsonSerializerOptions _serializerOptions = new() { - Converters = - { - new JsonStringEnumConverter() - }, - PropertyNameCaseInsensitive = true + Converters = { new JsonStringEnumConverter() }, + PropertyNameCaseInsensitive = true, }; public AreaTests(TestWebApplicationFactory factory) { - _client = factory.CreateClient(new WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false, - BaseAddress = new Uri("https://localhost:8000") - }); + _client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + BaseAddress = new Uri("https://localhost:8000"), + } + ); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( TestAuthHandler.AuthenticationScheme ); - object? context = factory.Services.GetService(typeof(FlotillaDbContext)) as FlotillaDbContext ?? throw new ArgumentNullException(nameof(factory)); + object? context = + factory.Services.GetService(typeof(FlotillaDbContext)) as FlotillaDbContext + ?? throw new ArgumentNullException(nameof(factory)); _databaseUtilities = new DatabaseUtilities((FlotillaDbContext)context); - } [Fact] @@ -56,22 +57,22 @@ public async Task AreaTest() { X = 1, Y = 2, - Z = 2 + Z = 2, }, Orientation = new Orientation { X = 0, Y = 0, Z = 0, - W = 1 - } + W = 1, + }, }; string testInstallation = "TestInstallationAreaTest"; var installationQuery = new CreateInstallationQuery { InstallationCode = testInstallation, - Name = testInstallation + Name = testInstallation, }; string testPlant = "TestPlantAreaTest"; @@ -79,7 +80,7 @@ public async Task AreaTest() { InstallationCode = testInstallation, PlantCode = testPlant, - Name = testPlant + Name = testPlant, }; string testDeck = "testDeckAreaTest"; @@ -87,7 +88,7 @@ public async Task AreaTest() { InstallationCode = testInstallation, PlantCode = testPlant, - Name = testDeck + Name = testDeck, }; string testArea = "testAreaAreaTest"; @@ -97,7 +98,7 @@ public async Task AreaTest() PlantCode = testPlant, DeckName = testDeck, AreaName = testArea, - DefaultLocalizationPose = testPose + DefaultLocalizationPose = testPose, }; var installationContent = new StringContent( @@ -126,7 +127,10 @@ public async Task AreaTest() // Act string installationUrl = "/installations"; - var installationResponse = await _client.PostAsync(installationUrl, installationContent); + var installationResponse = await _client.PostAsync( + installationUrl, + installationContent + ); string plantUrl = "/plants"; var plantResponse = await _client.PostAsync(plantUrl, plantContent); string deckUrl = "/decks"; @@ -139,7 +143,9 @@ public async Task AreaTest() Assert.True(plantResponse.IsSuccessStatusCode); Assert.True(deckResponse.IsSuccessStatusCode); Assert.True(areaResponse.IsSuccessStatusCode); - var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); + var area = await areaResponse.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(area); } @@ -149,7 +155,10 @@ public async Task MissionIsCreatedInInspectionArea() // Arrange - Initialise area var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); // Arrange - Robot var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); @@ -161,7 +170,7 @@ public async Task MissionIsCreatedInInspectionArea() { AnalysisType = AnalysisType.CarSeal, InspectionTarget = new Position(), - InspectionType = InspectionType.Image + InspectionType = InspectionType.Image, }; var tasks = new List { @@ -170,8 +179,8 @@ public async Task MissionIsCreatedInInspectionArea() Inspection = inspection, TagId = "test", RobotPose = new Pose(), - TaskOrder = 0 - } + TaskOrder = 0, + }, }; var missionQuery = new CustomMissionQuery { @@ -180,7 +189,7 @@ public async Task MissionIsCreatedInInspectionArea() InstallationCode = installation.InstallationCode, InspectionAreaName = deck.Name, Name = testMissionName, - Tasks = tasks + Tasks = tasks, }; var missionContent = new StringContent( @@ -194,17 +203,25 @@ public async Task MissionIsCreatedInInspectionArea() var missionResponse = await _client.PostAsync(missionUrl, missionContent); Assert.True(missionResponse.IsSuccessStatusCode); - var mission = await missionResponse.Content.ReadFromJsonAsync(_serializerOptions); + var mission = await missionResponse.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(mission); Assert.NotNull(mission.MissionId); string inspectionAreaUrl = "/decks"; - var inspectionareaMissionsResponse = await _client.GetAsync(inspectionAreaUrl + $"/{deck.Id}/mission-definitions"); + var inspectionareaMissionsResponse = await _client.GetAsync( + inspectionAreaUrl + $"/{deck.Id}/mission-definitions" + ); // Assert Assert.True(inspectionareaMissionsResponse.IsSuccessStatusCode); - var missions = await inspectionareaMissionsResponse.Content.ReadFromJsonAsync>(_serializerOptions); + var missions = await inspectionareaMissionsResponse.Content.ReadFromJsonAsync< + IList + >(_serializerOptions); Assert.NotNull(missions); - Assert.Single(missions.Where(m => m.Id.Equals(mission.MissionId, StringComparison.Ordinal))); + Assert.Single( + missions.Where(m => m.Id.Equals(mission.MissionId, StringComparison.Ordinal)) + ); } [Fact] @@ -214,9 +231,9 @@ public async Task EmergencyDockTest() var installation = await _databaseUtilities.ReadOrNewInstallation(); string installationCode = installation.InstallationCode; - // Act - string goToDockingPositionUrl = $"/emergency-action/{installationCode}/abort-current-missions-and-send-all-robots-to-safe-zone"; + string goToDockingPositionUrl = + $"/emergency-action/{installationCode}/abort-current-missions-and-send-all-robots-to-safe-zone"; var missionResponse = await _client.PostAsync(goToDockingPositionUrl, null); // Assert @@ -233,7 +250,10 @@ public async Task UpdateDefaultLocalizationPoseOnDeck() // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); string deckId = deck.Id; @@ -246,16 +266,16 @@ public async Task UpdateDefaultLocalizationPoseOnDeck() { X = 1, Y = 2, - Z = 3 + Z = 3, }, Orientation = new Orientation { X = 0, Y = 0, Z = 0, - W = 1 - } - } + W = 1, + }, + }, }; var content = new StringContent( JsonSerializer.Serialize(query), @@ -266,13 +286,17 @@ public async Task UpdateDefaultLocalizationPoseOnDeck() // Act var putResponse = await _client.PutAsync(url, content); Assert.True(putResponse.IsSuccessStatusCode); - var putDeck = await putResponse.Content.ReadFromJsonAsync(_serializerOptions); + var putDeck = await putResponse.Content.ReadFromJsonAsync( + _serializerOptions + ); // Assert Assert.NotNull(putDeck); Assert.NotNull(putDeck.DefaultLocalizationPose); Assert.True(putDeck.DefaultLocalizationPose.Position.Z.Equals(query.Pose.Position.Z)); - Assert.True(putDeck.DefaultLocalizationPose.Orientation.W.Equals(query.Pose.Orientation.W)); + Assert.True( + putDeck.DefaultLocalizationPose.Orientation.W.Equals(query.Pose.Orientation.W) + ); } } } diff --git a/backend/api.test/Client/MissionTests.cs b/backend/api.test/Client/MissionTests.cs index 7c669d1e0..28a92276f 100644 --- a/backend/api.test/Client/MissionTests.cs +++ b/backend/api.test/Client/MissionTests.cs @@ -14,6 +14,7 @@ using Api.Test.Database; using Microsoft.AspNetCore.Mvc.Testing; using Xunit; + namespace Api.Test.Client { [Collection("Database collection")] @@ -24,25 +25,26 @@ public class MissionTests : IClassFixture> private readonly JsonSerializerOptions _serializerOptions = new() { - Converters = - { - new JsonStringEnumConverter() - }, - PropertyNameCaseInsensitive = true + Converters = { new JsonStringEnumConverter() }, + PropertyNameCaseInsensitive = true, }; public MissionTests(TestWebApplicationFactory factory) { - _client = factory.CreateClient(new WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false, - BaseAddress = new Uri("https://localhost:8000") - }); + _client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + BaseAddress = new Uri("https://localhost:8000"), + } + ); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( TestAuthHandler.AuthenticationScheme ); - object? context = factory.Services.GetService(typeof(FlotillaDbContext)) as FlotillaDbContext ?? throw new ArgumentNullException(nameof(factory)); + object? context = + factory.Services.GetService(typeof(FlotillaDbContext)) as FlotillaDbContext + ?? throw new ArgumentNullException(nameof(factory)); _databaseUtilities = new DatabaseUtilities((FlotillaDbContext)context); } @@ -65,7 +67,7 @@ public async Task ScheduleOneMissionTest() RobotId = robotId, InstallationCode = installation.InstallationCode, MissionSourceId = missionSourceId, - DesiredStartTime = DateTime.UtcNow + DesiredStartTime = DateTime.UtcNow, }; var content = new StringContent( JsonSerializer.Serialize(query), @@ -77,7 +79,9 @@ public async Task ScheduleOneMissionTest() // Assert Assert.True(response.IsSuccessStatusCode); - var missionRun = await response.Content.ReadFromJsonAsync(_serializerOptions); + var missionRun = await response.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(missionRun); Assert.NotNull(missionRun.Id); Assert.Equal(MissionStatus.Pending, missionRun.Status); @@ -101,7 +105,7 @@ public async Task Schedule3MissionsTest() RobotId = robotId, InstallationCode = installation.InstallationCode, MissionSourceId = missionSourceId, - DesiredStartTime = DateTime.UtcNow + DesiredStartTime = DateTime.UtcNow, }; var content = new StringContent( JsonSerializer.Serialize(query), @@ -126,17 +130,23 @@ public async Task Schedule3MissionsTest() Assert.True(response.IsSuccessStatusCode); response = await _client.PostAsync(missionsUrl, content); - var missionRun1 = await response.Content.ReadFromJsonAsync(_serializerOptions); + var missionRun1 = await response.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.True(response.IsSuccessStatusCode); Assert.NotNull(missionRun1); response = await _client.PostAsync(missionsUrl, content); - var missionRun2 = await response.Content.ReadFromJsonAsync(_serializerOptions); + var missionRun2 = await response.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.True(response.IsSuccessStatusCode); Assert.NotNull(missionRun2); response = await _client.PostAsync(missionsUrl, content); - var missionRun3 = await response.Content.ReadFromJsonAsync(_serializerOptions); + var missionRun3 = await response.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.True(response.IsSuccessStatusCode); Assert.NotNull(missionRun3); @@ -159,8 +169,15 @@ public async Task AddNonDuplicateAreasToDb() // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); - var _ = await _databaseUtilities.ReadOrNewArea(installation.InstallationCode, plant.PlantCode, deck.Name); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); + var _ = await _databaseUtilities.ReadOrNewArea( + installation.InstallationCode, + plant.PlantCode, + deck.Name + ); var testPose = new Pose { @@ -168,15 +185,15 @@ public async Task AddNonDuplicateAreasToDb() { X = 1, Y = 2, - Z = 2 + Z = 2, }, Orientation = new Orientation { X = 0, Y = 0, Z = 0, - W = 1 - } + W = 1, + }, }; var areaQuery = new CreateAreaQuery { @@ -184,7 +201,7 @@ public async Task AddNonDuplicateAreasToDb() PlantCode = plant.PlantCode, DeckName = deck.Name, AreaName = "AddNonDuplicateAreasToDb_Area", - DefaultLocalizationPose = testPose + DefaultLocalizationPose = testPose, }; var areaContent = new StringContent( JsonSerializer.Serialize(areaQuery), @@ -193,10 +210,15 @@ public async Task AddNonDuplicateAreasToDb() ); string areaUrl = "/areas"; var response = await _client.PostAsync(areaUrl, areaContent); - Assert.True(response.IsSuccessStatusCode, $"Failed to post to {areaUrl}. Status code: {response.StatusCode}"); + Assert.True( + response.IsSuccessStatusCode, + $"Failed to post to {areaUrl}. Status code: {response.StatusCode}" + ); Assert.True(response != null, $"Failed to post to {areaUrl}. Null returned"); - var responseObject = await response.Content.ReadFromJsonAsync(_serializerOptions); + var responseObject = await response.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.True(responseObject != null, $"No object returned from post to {areaUrl}"); } @@ -206,8 +228,15 @@ public async Task AddDuplicateAreasToDb_Fails() // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); - var area = await _databaseUtilities.ReadOrNewArea(installation.InstallationCode, plant.PlantCode, deck.Name); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); + var area = await _databaseUtilities.ReadOrNewArea( + installation.InstallationCode, + plant.PlantCode, + deck.Name + ); var testPose = new Pose { @@ -215,15 +244,15 @@ public async Task AddDuplicateAreasToDb_Fails() { X = 1, Y = 2, - Z = 2 + Z = 2, }, Orientation = new Orientation { X = 0, Y = 0, Z = 0, - W = 1 - } + W = 1, + }, }; var areaQuery = new CreateAreaQuery { @@ -231,7 +260,7 @@ public async Task AddDuplicateAreasToDb_Fails() PlantCode = plant.PlantCode, DeckName = deck.Name, AreaName = area.Name, - DefaultLocalizationPose = testPose + DefaultLocalizationPose = testPose, }; var areaContent = new StringContent( JsonSerializer.Serialize(areaQuery), @@ -267,7 +296,10 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); string testMissionName = "testMissionScheduleDuplicateCustomMissionDefinitions"; @@ -284,16 +316,17 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() DesiredStartTime = DateTime.SpecifyKind(new DateTime(3050, 1, 1), DateTimeKind.Utc), InspectionFrequency = new TimeSpan(14, 0, 0, 0), Name = testMissionName, - Tasks = [ + Tasks = + [ new() { RobotPose = new Pose(new Position(23, 14, 4), new Orientation()), Inspection = new CustomInspectionQuery { InspectionTarget = new Position(), - InspectionType = InspectionType.Image + InspectionType = InspectionType.Image, }, - TaskOrder = 0 + TaskOrder = 0, }, new() { @@ -301,11 +334,11 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() Inspection = new CustomInspectionQuery { InspectionTarget = new Position(), - InspectionType = InspectionType.Image + InspectionType = InspectionType.Image, }, - TaskOrder = 1 - } - ] + TaskOrder = 1, + }, + ], }; var content = new StringContent( JsonSerializer.Serialize(query), @@ -321,8 +354,12 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() // Assert Assert.True(response1.IsSuccessStatusCode); Assert.True(response2.IsSuccessStatusCode); - var missionRun1 = await response1.Content.ReadFromJsonAsync(_serializerOptions); - var missionRun2 = await response2.Content.ReadFromJsonAsync(_serializerOptions); + var missionRun1 = await response1.Content.ReadFromJsonAsync( + _serializerOptions + ); + var missionRun2 = await response2.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(missionRun1); Assert.NotNull(missionRun2); string? missionId1 = missionRun1.MissionId; @@ -331,7 +368,9 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() // Increasing pageSize to 50 to ensure the missions we are looking for is included string missionDefinitionsUrl = "/missions/definitions?pageSize=50"; var missionDefinitionsResponse = await _client.GetAsync(missionDefinitionsUrl); - var missionDefinitions = await missionDefinitionsResponse.Content.ReadFromJsonAsync>(_serializerOptions); + var missionDefinitions = await missionDefinitionsResponse.Content.ReadFromJsonAsync< + List + >(_serializerOptions); Assert.NotNull(missionDefinitions); Assert.Single(missionDefinitions.Where(m => m.Id == missionId1)); } @@ -342,7 +381,10 @@ public async Task GetNextRun() // Arrange - Initialise area var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); // Arrange - Robot var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); @@ -358,18 +400,19 @@ public async Task GetNextRun() DesiredStartTime = DateTime.SpecifyKind(new DateTime(3050, 1, 1), DateTimeKind.Utc), InspectionFrequency = new TimeSpan(14, 0, 0, 0), Name = testMissionName, - Tasks = [ + Tasks = + [ new() { RobotPose = new Pose(), Inspection = new CustomInspectionQuery { InspectionTarget = new Position(), - InspectionType = InspectionType.Image + InspectionType = InspectionType.Image, }, - TaskOrder = 0 - } - ] + TaskOrder = 0, + }, + ], }; var content = new StringContent( JsonSerializer.Serialize(query), @@ -380,7 +423,9 @@ public async Task GetNextRun() string customMissionsUrl = "/missions/custom"; var response = await _client.PostAsync(customMissionsUrl, content); Assert.True(response.IsSuccessStatusCode); - var missionRun = await response.Content.ReadFromJsonAsync(_serializerOptions); + var missionRun = await response.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(missionRun); Assert.NotNull(missionRun.MissionId); Assert.NotNull(missionRun.Id); @@ -418,12 +463,27 @@ public async Task GetNextRun() "application/json" ); string scheduleMissionsUrl = $"/missions/schedule/{missionRun.MissionId}"; - var missionRun1Response = await _client.PostAsync(scheduleMissionsUrl, scheduleContent1); - var missionRun2Response = await _client.PostAsync(scheduleMissionsUrl, scheduleContent2); - var missionRun3Response = await _client.PostAsync(scheduleMissionsUrl, scheduleContent3); - var missionRun1 = await missionRun1Response.Content.ReadFromJsonAsync(_serializerOptions); - var missionRun2 = await missionRun2Response.Content.ReadFromJsonAsync(_serializerOptions); - var missionRun3 = await missionRun3Response.Content.ReadFromJsonAsync(_serializerOptions); + var missionRun1Response = await _client.PostAsync( + scheduleMissionsUrl, + scheduleContent1 + ); + var missionRun2Response = await _client.PostAsync( + scheduleMissionsUrl, + scheduleContent2 + ); + var missionRun3Response = await _client.PostAsync( + scheduleMissionsUrl, + scheduleContent3 + ); + var missionRun1 = await missionRun1Response.Content.ReadFromJsonAsync( + _serializerOptions + ); + var missionRun2 = await missionRun2Response.Content.ReadFromJsonAsync( + _serializerOptions + ); + var missionRun3 = await missionRun3Response.Content.ReadFromJsonAsync( + _serializerOptions + ); // Act string nextMissionUrl = $"missions/definitions/{missionRun.MissionId}/next-run"; @@ -431,7 +491,9 @@ public async Task GetNextRun() // Assert Assert.True(nextMissionResponse.IsSuccessStatusCode); - var nextMissionRun = await nextMissionResponse.Content.ReadFromJsonAsync(_serializerOptions); + var nextMissionRun = await nextMissionResponse.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(nextMissionRun); Assert.NotNull(missionRun1); Assert.NotNull(missionRun2); @@ -448,8 +510,15 @@ public async Task ScheduleDuplicatMissionDefinitions() // Arrange - Initialise area var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); - var area = await _databaseUtilities.ReadOrNewArea(installation.InstallationCode, plant.PlantCode, deck.Name); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); + var area = await _databaseUtilities.ReadOrNewArea( + installation.InstallationCode, + plant.PlantCode, + deck.Name + ); // Arrange - Robot var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); @@ -463,7 +532,7 @@ public async Task ScheduleDuplicatMissionDefinitions() RobotId = robotId, InstallationCode = installation.InstallationCode, MissionSourceId = missionSourceId, - DesiredStartTime = DateTime.UtcNow + DesiredStartTime = DateTime.UtcNow, }; var content = new StringContent( JsonSerializer.Serialize(query), @@ -479,8 +548,12 @@ public async Task ScheduleDuplicatMissionDefinitions() // Assert Assert.True(response1.IsSuccessStatusCode); Assert.True(response2.IsSuccessStatusCode); - var missionRun1 = await response1.Content.ReadFromJsonAsync(_serializerOptions); - var missionRun2 = await response2.Content.ReadFromJsonAsync(_serializerOptions); + var missionRun1 = await response1.Content.ReadFromJsonAsync( + _serializerOptions + ); + var missionRun2 = await response2.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(missionRun1); Assert.NotNull(missionRun2); string? missionId1 = missionRun1.MissionId; @@ -488,7 +561,9 @@ public async Task ScheduleDuplicatMissionDefinitions() Assert.Equal(missionId1, missionId2); string missionDefinitionsUrl = "/missions/definitions?pageSize=50"; var missionDefinitionsResponse = await _client.GetAsync(missionDefinitionsUrl); - var missionDefinitions = await missionDefinitionsResponse.Content.ReadFromJsonAsync>(_serializerOptions); + var missionDefinitions = await missionDefinitionsResponse.Content.ReadFromJsonAsync< + List + >(_serializerOptions); Assert.NotNull(missionDefinitions); Assert.NotNull(missionDefinitions.Find(m => m.Id == missionId1)); } @@ -499,12 +574,17 @@ public async Task MissionDoesNotStartIfRobotIsNotInSameInstallationAsMission() // Arrange - Initialise area var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); - string testMissionName = "testMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission"; + string testMissionName = + "testMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission"; // Arrange - Get different installation - string otherInstallationCode = "installationMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission_Other"; + string otherInstallationCode = + "installationMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission_Other"; var otherInstallation = await _databaseUtilities.NewInstallation(otherInstallationCode); // Arrange - Robot @@ -520,16 +600,17 @@ public async Task MissionDoesNotStartIfRobotIsNotInSameInstallationAsMission() DesiredStartTime = DateTime.SpecifyKind(new DateTime(3050, 1, 1), DateTimeKind.Utc), InspectionFrequency = new TimeSpan(14, 0, 0, 0), Name = testMissionName, - Tasks = [ + Tasks = + [ new() { RobotPose = new Pose(), Inspection = new CustomInspectionQuery { InspectionTarget = new Position(), - InspectionType = InspectionType.Image + InspectionType = InspectionType.Image, }, - TaskOrder = 0 + TaskOrder = 0, }, new() { @@ -537,11 +618,11 @@ public async Task MissionDoesNotStartIfRobotIsNotInSameInstallationAsMission() Inspection = new CustomInspectionQuery { InspectionTarget = new Position(), - InspectionType = InspectionType.Image + InspectionType = InspectionType.Image, }, - TaskOrder = 1 - } - ] + TaskOrder = 1, + }, + ], }; var content = new StringContent( JsonSerializer.Serialize(query), @@ -563,15 +644,27 @@ public async Task MissionFailsIfRobotIsNotInSameDeckAsMission() var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); string deckName1 = "deckMissionFailsIfRobotIsNotInSameDeckAsMission1"; - var deck1 = await _databaseUtilities.NewDeck(installation.InstallationCode, plant.PlantCode, deckName1); + var deck1 = await _databaseUtilities.NewDeck( + installation.InstallationCode, + plant.PlantCode, + deckName1 + ); string deckName2 = "deckMissionFailsIfRobotIsNotInSameDeckAsMission2"; - var deck2 = await _databaseUtilities.NewDeck(installation.InstallationCode, plant.PlantCode, deckName2); + var deck2 = await _databaseUtilities.NewDeck( + installation.InstallationCode, + plant.PlantCode, + deckName2 + ); string testMissionName = "testMissionFailsIfRobotIsNotInSameDeckAsMission"; // Arrange - Robot - var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, deck1); + var robot = await _databaseUtilities.NewRobot( + RobotStatus.Available, + installation, + deck1 + ); string robotId = robot.Id; // Arrange - Mission Run Query @@ -583,16 +676,17 @@ public async Task MissionFailsIfRobotIsNotInSameDeckAsMission() DesiredStartTime = DateTime.SpecifyKind(new DateTime(3050, 1, 1), DateTimeKind.Utc), InspectionFrequency = new TimeSpan(14, 0, 0, 0), Name = testMissionName, - Tasks = [ + Tasks = + [ new() { RobotPose = new Pose(new Position(1, 9, 4), new Orientation()), Inspection = new CustomInspectionQuery { InspectionTarget = new Position(), - InspectionType = InspectionType.Image + InspectionType = InspectionType.Image, }, - TaskOrder = 0 + TaskOrder = 0, }, new() { @@ -600,11 +694,11 @@ public async Task MissionFailsIfRobotIsNotInSameDeckAsMission() Inspection = new CustomInspectionQuery { InspectionTarget = new Position(), - InspectionType = InspectionType.Image + InspectionType = InspectionType.Image, }, - TaskOrder = 1 - } - ] + TaskOrder = 1, + }, + ], }; var content = new StringContent( JsonSerializer.Serialize(query), diff --git a/backend/api.test/Client/RobotTests.cs b/backend/api.test/Client/RobotTests.cs index 89ea9ba01..b88d8c6fe 100644 --- a/backend/api.test/Client/RobotTests.cs +++ b/backend/api.test/Client/RobotTests.cs @@ -14,6 +14,7 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Xunit; + namespace Api.Test.Client { [Collection("Database collection")] @@ -24,24 +25,25 @@ public class RobotTests : IClassFixture> private readonly JsonSerializerOptions _serializerOptions = new() { - Converters = - { - new JsonStringEnumConverter() - }, - PropertyNameCaseInsensitive = true + Converters = { new JsonStringEnumConverter() }, + PropertyNameCaseInsensitive = true, }; public RobotTests(TestWebApplicationFactory factory) { - _client = factory.CreateClient(new WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false, - BaseAddress = new Uri("https://localhost:8000") - }); + _client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + BaseAddress = new Uri("https://localhost:8000"), + } + ); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( TestAuthHandler.AuthenticationScheme ); - object? context = factory.Services.GetService(typeof(FlotillaDbContext)) as FlotillaDbContext ?? throw new ArgumentNullException(nameof(factory)); + object? context = + factory.Services.GetService(typeof(FlotillaDbContext)) as FlotillaDbContext + ?? throw new ArgumentNullException(nameof(factory)); _databaseUtilities = new DatabaseUtilities((FlotillaDbContext)context); } @@ -50,7 +52,9 @@ public async Task RobotsTest() { string url = "/robots"; var response = await _client.GetAsync(url); - var robots = await response.Content.ReadFromJsonAsync>(_serializerOptions); + var robots = await response.Content.ReadFromJsonAsync>( + _serializerOptions + ); Assert.True(response.IsSuccessStatusCode); Assert.NotNull(robots); } @@ -72,21 +76,26 @@ public async Task GetRobotById_ShouldReturnRobot() string url = "/robots"; var response = await _client.GetAsync(url); - var robots = await response.Content.ReadFromJsonAsync>(_serializerOptions); + var robots = await response.Content.ReadFromJsonAsync>( + _serializerOptions + ); Assert.NotNull(robots); string robotId = robots[0].Id; var robotResponse = await _client.GetAsync("/robots/" + robotId); - var robot = await robotResponse.Content.ReadFromJsonAsync(_serializerOptions); + var robot = await robotResponse.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.Equal(HttpStatusCode.OK, robotResponse.StatusCode); Assert.NotNull(robot); Assert.Equal(robot.Id, robotId); } - #pragma warning disable xUnit1004 - [Fact(Skip = "Runs inconcistently as it is tied to the database interactions of other tests")] + [Fact( + Skip = "Runs inconcistently as it is tied to the database interactions of other tests" + )] #pragma warning restore xUnit1004 public async Task RobotIsNotCreatedWithAreaNotInInstallation() { @@ -94,8 +103,13 @@ public async Task RobotIsNotCreatedWithAreaNotInInstallation() var installation = await _databaseUtilities.ReadOrNewInstallation(); var wrongInstallation = await _databaseUtilities.NewInstallation("wrongInstallation"); - var wrongPlant = await _databaseUtilities.ReadOrNewPlant(wrongInstallation.InstallationCode); - var wrongDeck = await _databaseUtilities.ReadOrNewDeck(wrongInstallation.InstallationCode, wrongPlant.PlantCode); + var wrongPlant = await _databaseUtilities.ReadOrNewPlant( + wrongInstallation.InstallationCode + ); + var wrongDeck = await _databaseUtilities.ReadOrNewDeck( + wrongInstallation.InstallationCode, + wrongPlant.PlantCode + ); // Arrange - Create robot var robotQuery = new CreateRobotQuery @@ -124,7 +138,10 @@ public async Task RobotIsNotCreatedWithAreaNotInInstallation() } catch (DbUpdateException ex) { - Assert.True(ex.Message == $"Could not create new robot in database as inspection area '{wrongDeck.Name}' does not exist in installation {installation.InstallationCode}"); + Assert.True( + ex.Message + == $"Could not create new robot in database as inspection area '{wrongDeck.Name}' does not exist in installation {installation.InstallationCode}" + ); } } } diff --git a/backend/api.test/Client/RoleAccessTests.cs b/backend/api.test/Client/RoleAccessTests.cs index b0d575a27..df8217bdb 100644 --- a/backend/api.test/Client/RoleAccessTests.cs +++ b/backend/api.test/Client/RoleAccessTests.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Xunit; + namespace Api.Test { [Collection("Database collection")] @@ -22,23 +23,23 @@ public class RoleAccessTests : IClassFixture> private readonly JsonSerializerOptions _serializerOptions = new() { - Converters = - { - new JsonStringEnumConverter() - }, - PropertyNameCaseInsensitive = true + Converters = { new JsonStringEnumConverter() }, + PropertyNameCaseInsensitive = true, }; public RoleAccessTests(TestWebApplicationFactory factory) { - _httpContextAccessor = (MockHttpContextAccessor)factory.Services.GetService()!; + _httpContextAccessor = (MockHttpContextAccessor) + factory.Services.GetService()!; _httpContextAccessor.SetHttpContextRoles(["Role.Admin"]); //var x = new HttpContextAccessor(); - _client = factory.CreateClient(new WebApplicationFactoryClientOptions - { - AllowAutoRedirect = false, - BaseAddress = new Uri("https://localhost:8000") - }); + _client = factory.CreateClient( + new WebApplicationFactoryClientOptions + { + AllowAutoRedirect = false, + BaseAddress = new Uri("https://localhost:8000"), + } + ); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( TestAuthHandler.AuthenticationScheme ); @@ -52,7 +53,7 @@ public async Task AuthorisedGetPlantTest_NotFound() { InstallationCode = "AuthorisedGetPlantTest_NotFoundInstallation", RoleName = "User.AuthorisedGetPlantTest_NotFoundInstallation", - AccessLevel = RoleAccessLevel.USER + AccessLevel = RoleAccessLevel.USER, }; var accessRoleContent = new StringContent( JsonSerializer.Serialize(accessRoleQuery), @@ -64,7 +65,7 @@ public async Task AuthorisedGetPlantTest_NotFound() var installationQuery = new CreateInstallationQuery { InstallationCode = testInstallation, - Name = testInstallation + Name = testInstallation, }; string testPlant = "AuthorisedGetPlantTest_NotFoundPlant"; @@ -72,7 +73,7 @@ public async Task AuthorisedGetPlantTest_NotFound() { InstallationCode = testInstallation, PlantCode = testPlant, - Name = testPlant + Name = testPlant, }; var installationContent = new StringContent( @@ -89,7 +90,10 @@ public async Task AuthorisedGetPlantTest_NotFound() // Act string installationUrl = "/installations"; - var installationResponse = await _client.PostAsync(installationUrl, installationContent); + var installationResponse = await _client.PostAsync( + installationUrl, + installationContent + ); string accessRoleUrl = "/access-roles"; var accessRoleResponse = await _client.PostAsync(accessRoleUrl, accessRoleContent); string plantUrl = "/plants"; @@ -121,9 +125,11 @@ public async Task ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTest() // Arrange var accessRoleQuery = new CreateAccessRoleQuery { - InstallationCode = "ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestInstallation", - RoleName = "User.ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestInstallation", - AccessLevel = RoleAccessLevel.USER + InstallationCode = + "ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestInstallation", + RoleName = + "User.ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestInstallation", + AccessLevel = RoleAccessLevel.USER, }; var accessRoleContent = new StringContent( JsonSerializer.Serialize(accessRoleQuery), @@ -137,22 +143,23 @@ public async Task ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTest() { X = 1, Y = 2, - Z = 2 + Z = 2, }, Orientation = new Orientation { X = 0, Y = 0, Z = 0, - W = 1 - } + W = 1, + }, }; - string testInstallation = "ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestInstallation"; + string testInstallation = + "ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestInstallation"; var installationQuery = new CreateInstallationQuery { InstallationCode = testInstallation, - Name = testInstallation + Name = testInstallation, }; string testPlant = "ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestPlant"; @@ -160,7 +167,7 @@ public async Task ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTest() { InstallationCode = testInstallation, PlantCode = testPlant, - Name = testPlant + Name = testPlant, }; string testDeck = "ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestDeck"; @@ -168,7 +175,7 @@ public async Task ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTest() { InstallationCode = testInstallation, PlantCode = testPlant, - Name = testDeck + Name = testDeck, }; string testArea = "ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestArea"; @@ -178,7 +185,7 @@ public async Task ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTest() PlantCode = testPlant, DeckName = testDeck, AreaName = testArea, - DefaultLocalizationPose = testPose + DefaultLocalizationPose = testPose, }; var installationContent = new StringContent( @@ -207,7 +214,10 @@ public async Task ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTest() // Act string installationUrl = "/installations"; - var installationResponse = await _client.PostAsync(installationUrl, installationContent); + var installationResponse = await _client.PostAsync( + installationUrl, + installationContent + ); string accessRoleUrl = "/access-roles"; var accessRoleResponse = await _client.PostAsync(accessRoleUrl, accessRoleContent); string plantUrl = "/plants"; @@ -218,7 +228,9 @@ public async Task ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTest() var areaResponse = await _client.PostAsync(areaUrl, areaContent); // Only restrict ourselves to non-admin role after doing POSTs - _httpContextAccessor.SetHttpContextRoles(["User.ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestInstallation"]); + _httpContextAccessor.SetHttpContextRoles( + ["User.ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTestInstallation"] + ); // Assert Assert.True(accessRoleResponse.IsSuccessStatusCode); @@ -226,7 +238,9 @@ public async Task ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTest() Assert.True(plantResponse.IsSuccessStatusCode); Assert.True(deckResponse.IsSuccessStatusCode); Assert.True(areaResponse.IsSuccessStatusCode); - var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); + var area = await areaResponse.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(area); // Act @@ -235,7 +249,9 @@ public async Task ExplicitlyAuthorisedPostInstallationPlantDeckAndAreaTest() // Assert Assert.True(sameAreaResponse.IsSuccessStatusCode); - var sameArea = await sameAreaResponse.Content.ReadFromJsonAsync(_serializerOptions); + var sameArea = await sameAreaResponse.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(sameArea); Assert.Equal(sameArea.Id, area.Id); } @@ -250,22 +266,23 @@ public async Task AdminAuthorisedPostInstallationPlantDeckAndAreaTest() { X = 1, Y = 2, - Z = 2 + Z = 2, }, Orientation = new Orientation { X = 0, Y = 0, Z = 0, - W = 1 - } + W = 1, + }, }; - string testInstallation = "AdminAuthorisedPostInstallationPlantDeckAndAreaTestInstallation"; + string testInstallation = + "AdminAuthorisedPostInstallationPlantDeckAndAreaTestInstallation"; var installationQuery = new CreateInstallationQuery { InstallationCode = testInstallation, - Name = testInstallation + Name = testInstallation, }; string testPlant = "AdminAuthorisedPostInstallationPlantDeckAndAreaTestPlant"; @@ -273,7 +290,7 @@ public async Task AdminAuthorisedPostInstallationPlantDeckAndAreaTest() { InstallationCode = testInstallation, PlantCode = testPlant, - Name = testPlant + Name = testPlant, }; string testDeck = "AdminAuthorisedPostInstallationPlantDeckAndAreaTestDeck"; @@ -281,7 +298,7 @@ public async Task AdminAuthorisedPostInstallationPlantDeckAndAreaTest() { InstallationCode = testInstallation, PlantCode = testPlant, - Name = testDeck + Name = testDeck, }; string testArea = "AdminAuthorisedPostInstallationPlantDeckAndAreaTestArea"; @@ -291,7 +308,7 @@ public async Task AdminAuthorisedPostInstallationPlantDeckAndAreaTest() PlantCode = testPlant, DeckName = testDeck, AreaName = testArea, - DefaultLocalizationPose = testPose + DefaultLocalizationPose = testPose, }; var installationContent = new StringContent( @@ -320,7 +337,10 @@ public async Task AdminAuthorisedPostInstallationPlantDeckAndAreaTest() // Act string installationUrl = "/installations"; - var installationResponse = await _client.PostAsync(installationUrl, installationContent); + var installationResponse = await _client.PostAsync( + installationUrl, + installationContent + ); string plantUrl = "/plants"; var plantResponse = await _client.PostAsync(plantUrl, plantContent); string deckUrl = "/decks"; @@ -333,7 +353,9 @@ public async Task AdminAuthorisedPostInstallationPlantDeckAndAreaTest() Assert.True(plantResponse.IsSuccessStatusCode); Assert.True(deckResponse.IsSuccessStatusCode); Assert.True(areaResponse.IsSuccessStatusCode); - var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); + var area = await areaResponse.Content.ReadFromJsonAsync( + _serializerOptions + ); Assert.NotNull(area); } } diff --git a/backend/api.test/Database/DatabaseUtilities.cs b/backend/api.test/Database/DatabaseUtilities.cs index 0eba682b1..dd26e8198 100644 --- a/backend/api.test/Database/DatabaseUtilities.cs +++ b/backend/api.test/Database/DatabaseUtilities.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Moq; + namespace Api.Test.Database { public class DatabaseUtilities @@ -28,21 +29,58 @@ public class DatabaseUtilities private readonly string _testDeckName = "testDeck"; private readonly string _testAreaName = "testArea"; - public DatabaseUtilities(FlotillaDbContext context) { var defaultLocalizationPoseService = new DefaultLocalizationPoseService(context); _accessRoleService = new AccessRoleService(context, new HttpContextAccessor()); _installationService = new InstallationService(context, _accessRoleService); - _missionTaskService = new MissionTaskService(context, new Mock>().Object); + _missionTaskService = new MissionTaskService( + context, + new Mock>().Object + ); _plantService = new PlantService(context, _installationService, _accessRoleService); - _deckService = new DeckService(context, defaultLocalizationPoseService, _installationService, _plantService, _accessRoleService, new MockSignalRService()); - _areaService = new AreaService(context, _installationService, _plantService, _deckService, defaultLocalizationPoseService, _accessRoleService); - _userInfoService = new UserInfoService(context, new HttpContextAccessor(), new Mock>().Object); + _deckService = new DeckService( + context, + defaultLocalizationPoseService, + _installationService, + _plantService, + _accessRoleService, + new MockSignalRService() + ); + _areaService = new AreaService( + context, + _installationService, + _plantService, + _deckService, + defaultLocalizationPoseService, + _accessRoleService + ); + _userInfoService = new UserInfoService( + context, + new HttpContextAccessor(), + new Mock>().Object + ); _robotModelService = new RobotModelService(context); - _robotService = new RobotService(context, new Mock>().Object, _robotModelService, new MockSignalRService(), _accessRoleService, _installationService, _deckService); - _missionRunService = new MissionRunService(context, new MockSignalRService(), new Mock>().Object, _accessRoleService, _missionTaskService, _deckService, _robotService, _userInfoService); + _robotService = new RobotService( + context, + new Mock>().Object, + _robotModelService, + new MockSignalRService(), + _accessRoleService, + _installationService, + _deckService + ); + _missionRunService = new MissionRunService( + context, + new MockSignalRService(), + new Mock>().Object, + _accessRoleService, + _missionTaskService, + _deckService, + _robotService, + _userInfoService + ); _sourceService = new SourceService(context, new Mock>().Object); } @@ -57,7 +95,8 @@ public async Task NewMissionRun( Api.Database.Models.TaskStatus taskStatus = Api.Database.Models.TaskStatus.Successful ) { - if (string.IsNullOrEmpty(isarMissionId)) isarMissionId = Guid.NewGuid().ToString(); + if (string.IsNullOrEmpty(isarMissionId)) + isarMissionId = Guid.NewGuid().ToString(); var missionRun = new MissionRun { Name = "testMission", @@ -69,22 +108,16 @@ public async Task NewMissionRun( DesiredStartTime = DateTime.Now, InspectionArea = inspectionArea, Tasks = [], - InstallationCode = installationCode + InstallationCode = installationCode, }; if (missionRunType == MissionRunType.ReturnHome) { - missionRun.Tasks = - [ - new(new Pose(), MissionTaskType.ReturnHome) - ]; + missionRun.Tasks = [new(new Pose(), MissionTaskType.ReturnHome)]; } else { - missionRun.Tasks = - [ - new(new Pose(), MissionTaskType.Inspection) - ]; + missionRun.Tasks = [new(new Pose(), MissionTaskType.Inspection)]; } if (writeToDatabase) { @@ -95,24 +128,30 @@ public async Task NewMissionRun( public async Task ReadOrNewInstallation() { - if (await _installationService.ReadByInstallationCode(_testInstallationCode) is Installation installation) return installation; + if ( + await _installationService.ReadByInstallationCode(_testInstallationCode) + is Installation installation + ) + return installation; return await NewInstallation(); } public async Task NewInstallation(string installationCode = "") { - if (string.IsNullOrEmpty(installationCode)) installationCode = _testInstallationCode; + if (string.IsNullOrEmpty(installationCode)) + installationCode = _testInstallationCode; var createInstallationQuery = new CreateInstallationQuery { InstallationCode = installationCode, - Name = _testInstallationName + Name = _testInstallationName, }; return await _installationService.Create(createInstallationQuery); } public async Task ReadOrNewPlant(string installationCode) { - if (await _plantService.ReadByPlantCode(_testPlantCode) is Plant plant) return plant; + if (await _plantService.ReadByPlantCode(_testPlantCode) is Plant plant) + return plant; return await NewPlant(installationCode); } @@ -122,7 +161,7 @@ public async Task NewPlant(string installationCode) { InstallationCode = installationCode, PlantCode = _testPlantCode, - Name = "testPlant" + Name = "testPlant", }; return await _plantService.Create(createPlantQuery); @@ -130,49 +169,73 @@ public async Task NewPlant(string installationCode) public async Task ReadOrNewDeck(string installationCode, string plantCode) { - if (await _deckService.ReadByInstallationAndName(installationCode, _testDeckName) is Deck deck) return deck; + if ( + await _deckService.ReadByInstallationAndName(installationCode, _testDeckName) + is Deck deck + ) + return deck; return await NewDeck(installationCode, plantCode); } - public async Task NewDeck(string installationCode, string plantCode, string deckName = "") + public async Task NewDeck( + string installationCode, + string plantCode, + string deckName = "" + ) { - if (string.IsNullOrEmpty(deckName)) deckName = _testDeckName; + if (string.IsNullOrEmpty(deckName)) + deckName = _testDeckName; var createDeckQuery = new CreateDeckQuery { InstallationCode = installationCode, PlantCode = plantCode, Name = deckName, - DefaultLocalizationPose = new CreateDefaultLocalizationPose() - { - Pose = new Pose() - } + DefaultLocalizationPose = new CreateDefaultLocalizationPose() { Pose = new Pose() }, }; return await _deckService.Create(createDeckQuery); } - public async Task ReadOrNewArea(string installationCode, string plantCode, string deckName) + public async Task ReadOrNewArea( + string installationCode, + string plantCode, + string deckName + ) { - if (await _areaService.ReadByInstallationAndName(_testInstallationCode, _testAreaName) is Area area) return area; + if ( + await _areaService.ReadByInstallationAndName(_testInstallationCode, _testAreaName) + is Area area + ) + return area; return await NewArea(installationCode, plantCode, deckName); } - public async Task NewArea(string installationCode, string plantCode, string deckName, string areaName = "") + public async Task NewArea( + string installationCode, + string plantCode, + string deckName, + string areaName = "" + ) { - if (string.IsNullOrEmpty(areaName)) areaName = _testAreaName; + if (string.IsNullOrEmpty(areaName)) + areaName = _testAreaName; var createAreaQuery = new CreateAreaQuery { InstallationCode = installationCode, PlantCode = plantCode, DeckName = deckName, AreaName = areaName, - DefaultLocalizationPose = new Pose() + DefaultLocalizationPose = new Pose(), }; return await _areaService.Create(createAreaQuery); } - public async Task NewRobot(RobotStatus status, Installation installation, Deck? inspectionArea = null) + public async Task NewRobot( + RobotStatus status, + Installation installation, + Deck? inspectionArea = null + ) { var createRobotQuery = new CreateRobotQuery { @@ -186,20 +249,25 @@ public async Task NewRobot(RobotStatus status, Installation installation, Host = "localhost", Port = 3000, Status = status, - RobotCapabilities = [RobotCapabilitiesEnum.take_image, RobotCapabilitiesEnum.return_to_home, RobotCapabilitiesEnum.localize] + RobotCapabilities = + [ + RobotCapabilitiesEnum.take_image, + RobotCapabilitiesEnum.return_to_home, + RobotCapabilitiesEnum.localize, + ], }; - var robotModel = await _robotModelService.ReadByRobotType(createRobotQuery.RobotType, readOnly: true); + var robotModel = await _robotModelService.ReadByRobotType( + createRobotQuery.RobotType, + readOnly: true + ); var robot = new Robot(createRobotQuery, installation, robotModel!, inspectionArea); return await _robotService.Create(robot); } public async Task NewSource(string sourceId) { - return await _sourceService.Create(new Source - { - SourceId = sourceId - }); + return await _sourceService.Create(new Source { SourceId = sourceId }); } } } diff --git a/backend/api.test/Database/Models.cs b/backend/api.test/Database/Models.cs index 7aeadd9fa..ea4ef5b47 100644 --- a/backend/api.test/Database/Models.cs +++ b/backend/api.test/Database/Models.cs @@ -18,7 +18,7 @@ public void TestRotationNorth() X = 0, Y = 0, Z = 0.7071F, - W = 0.7071F + W = 0.7071F, }; Assert.Equal( @@ -39,16 +39,12 @@ public void TestRotationSouth() X = 0, Y = 0, Z = -0.7071F, - W = 0.7071F + W = 0.7071F, }; var pose = new Pose(mockAngleAxisParameters, mockAngle); - Assert.Equal( - expected.Z, - pose.Orientation.Z, - 3.0 - ); + Assert.Equal(expected.Z, pose.Orientation.Z, 3.0); } [Fact] @@ -62,7 +58,7 @@ public void TestNegativaRotation() X = 0, Y = 0, Z = 1F, - W = 0 + W = 0, }; Assert.Equal( diff --git a/backend/api.test/DbContextTestSetup.cs b/backend/api.test/DbContextTestSetup.cs index 83e485ab4..4dcf5a974 100644 --- a/backend/api.test/DbContextTestSetup.cs +++ b/backend/api.test/DbContextTestSetup.cs @@ -23,7 +23,7 @@ private DbContextOptions CreateOptions() string connectionString = new SqliteConnectionStringBuilder { DataSource = ":memory:", - Cache = SqliteCacheMode.Shared + Cache = SqliteCacheMode.Shared, }.ToString(); _connection = new SqliteConnection(connectionString); _connection.Open(); @@ -67,9 +67,11 @@ public class TestAuthHandlerOptions : AuthenticationSchemeOptions } // Class for mocking authentication handler - public class TestAuthHandler(IOptionsMonitor options, - ILoggerFactory logger, - UrlEncoder encoder) : AuthenticationHandler(options, logger, encoder) + public class TestAuthHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder + ) : AuthenticationHandler(options, logger, encoder) { public const string AuthenticationScheme = "Test"; @@ -78,7 +80,7 @@ protected override Task HandleAuthenticateAsync() var claims = new[] { new Claim(ClaimTypes.Name, "Test.User"), - new Claim(ClaimTypes.Role, "Role.Admin") + new Claim(ClaimTypes.Role, "Role.Admin"), }; var identity = new ClaimsIdentity(claims, AuthenticationScheme); var principal = new ClaimsPrincipal(identity); diff --git a/backend/api.test/EventHandlers/ObjectExtensions.cs b/backend/api.test/EventHandlers/ObjectExtensions.cs index b515d822f..a08393307 100644 --- a/backend/api.test/EventHandlers/ObjectExtensions.cs +++ b/backend/api.test/EventHandlers/ObjectExtensions.cs @@ -1,13 +1,19 @@ using System; using System.Reflection; + namespace Api.Test.EventHandlers { public static class ObjectExtensions { - public static void RaiseEvent(this object target, string eventName, TEventArgs eventArgs) + public static void RaiseEvent( + this object target, + string eventName, + TEventArgs eventArgs + ) { var targetType = target.GetType(); - const BindingFlags BindingFlags = BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; + const BindingFlags BindingFlags = + BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance; var fi = targetType.GetField(eventName, BindingFlags); if (fi != null) { diff --git a/backend/api.test/EventHandlers/TestInspectionFindingsEventHandler.cs b/backend/api.test/EventHandlers/TestInspectionFindingsEventHandler.cs index ad38622c1..6845561c1 100644 --- a/backend/api.test/EventHandlers/TestInspectionFindingsEventHandler.cs +++ b/backend/api.test/EventHandlers/TestInspectionFindingsEventHandler.cs @@ -16,11 +16,15 @@ public void TestGenerateAdaptiveCard() var findingsReports = new List { new("Tag1", "Plant1", "Area1", "Finding1", DateTime.UtcNow), - new("Tag2", "Plant1", "Area2", "Finding2", DateTime.UtcNow) + new("Tag2", "Plant1", "Area2", "Finding2", DateTime.UtcNow), }; // Act - string adaptiveCardJson = InspectionFindingEventHandler.GenerateAdaptiveCard("Rapport", numberOfFindings, findingsReports); + string adaptiveCardJson = InspectionFindingEventHandler.GenerateAdaptiveCard( + "Rapport", + numberOfFindings, + findingsReports + ); // Assert Assert.NotNull(adaptiveCardJson); diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs index 335ad3a89..2dc7847a7 100644 --- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs +++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs @@ -44,13 +44,17 @@ public TestMissionEventHandler(DatabaseFixture fixture) var mqttServiceLogger = new Mock>().Object; var mqttEventHandlerLogger = new Mock>().Object; var missionLogger = new Mock>().Object; - var missionSchedulingServiceLogger = new Mock>().Object; + var missionSchedulingServiceLogger = new Mock< + ILogger + >().Object; var robotServiceLogger = new Mock>().Object; var localizationServiceLogger = new Mock>().Object; var mapServiceLogger = new Mock>().Object; var mapBlobOptions = new Mock>().Object; var returnToHomeServiceLogger = new Mock>().Object; - var missionDefinitionServiceLogger = new Mock>().Object; + var missionDefinitionServiceLogger = new Mock< + ILogger + >().Object; var lastMissionRunServiceLogger = new Mock>().Object; var sourceServiceLogger = new Mock>().Object; var errorHandlingServiceLogger = new Mock>().Object; @@ -62,7 +66,11 @@ public TestMissionEventHandler(DatabaseFixture fixture) var signalRService = new MockSignalRService(); var accessRoleService = new AccessRoleService(context, new HttpContextAccessor()); - var userInfoService = new UserInfoService(context, new HttpContextAccessor(), new Mock>().Object); + var userInfoService = new UserInfoService( + context, + new HttpContextAccessor(), + new Mock>().Object + ); _mqttService = new MqttService(mqttServiceLogger, configuration); @@ -77,17 +85,77 @@ public TestMissionEventHandler(DatabaseFixture fixture) var installationService = new InstallationService(context, accessRoleService); var defaultLocalizationPoseService = new DefaultLocalizationPoseService(context); var plantService = new PlantService(context, installationService, accessRoleService); - var deckService = new DeckService(context, defaultLocalizationPoseService, installationService, plantService, accessRoleService, signalRService); - var areaService = new AreaService(context, installationService, plantService, deckService, defaultLocalizationPoseService, accessRoleService); + var deckService = new DeckService( + context, + defaultLocalizationPoseService, + installationService, + plantService, + accessRoleService, + signalRService + ); + var areaService = new AreaService( + context, + installationService, + plantService, + deckService, + defaultLocalizationPoseService, + accessRoleService + ); var mapServiceMock = new MockMapService(); - _robotService = new RobotService(context, robotServiceLogger, robotModelService, signalRService, accessRoleService, installationService, deckService); - _missionRunService = new MissionRunService(context, signalRService, missionLogger, accessRoleService, missionTaskService, deckService, _robotService, userInfoService); - var missionDefinitionService = new MissionDefinitionService(context, missionLoader, signalRService, accessRoleService, missionDefinitionServiceLogger, _missionRunService, sourceService); - _localizationService = new LocalizationService(localizationServiceLogger, _robotService, installationService, deckService); - var errorHandlingService = new ErrorHandlingService(errorHandlingServiceLogger, _robotService, _missionRunService); - var returnToHomeService = new ReturnToHomeService(returnToHomeServiceLogger, _robotService, _missionRunService); - _missionSchedulingService = new MissionSchedulingService(missionSchedulingServiceLogger, _missionRunService, _robotService, - isarServiceMock, _localizationService, returnToHomeService, signalRService, errorHandlingService); + _robotService = new RobotService( + context, + robotServiceLogger, + robotModelService, + signalRService, + accessRoleService, + installationService, + deckService + ); + _missionRunService = new MissionRunService( + context, + signalRService, + missionLogger, + accessRoleService, + missionTaskService, + deckService, + _robotService, + userInfoService + ); + var missionDefinitionService = new MissionDefinitionService( + context, + missionLoader, + signalRService, + accessRoleService, + missionDefinitionServiceLogger, + _missionRunService, + sourceService + ); + _localizationService = new LocalizationService( + localizationServiceLogger, + _robotService, + installationService, + deckService + ); + var errorHandlingService = new ErrorHandlingService( + errorHandlingServiceLogger, + _robotService, + _missionRunService + ); + var returnToHomeService = new ReturnToHomeService( + returnToHomeServiceLogger, + _robotService, + _missionRunService + ); + _missionSchedulingService = new MissionSchedulingService( + missionSchedulingServiceLogger, + _missionRunService, + _robotService, + isarServiceMock, + _localizationService, + returnToHomeService, + signalRService, + errorHandlingService + ); var lastMissionRunService = new LastMissionRunService(missionDefinitionService); _databaseUtilities = new DatabaseUtilities(context); @@ -123,9 +191,7 @@ public TestMissionEventHandler(DatabaseFixture fixture) mockServiceProvider .Setup(p => p.GetService(typeof(ILastMissionRunService))) .Returns(lastMissionRunService); - mockServiceProvider - .Setup(p => p.GetService(typeof(IAreaService))) - .Returns(areaService); + mockServiceProvider.Setup(p => p.GetService(typeof(IAreaService))).Returns(areaService); mockServiceProvider .Setup(p => p.GetService(typeof(ISignalRService))) .Returns(signalRService); @@ -133,7 +199,6 @@ public TestMissionEventHandler(DatabaseFixture fixture) .Setup(p => p.GetService(typeof(IEmergencyActionService))) .Returns(_emergencyActionService); - // Mock service injector var mockScope = new Mock(); mockScope.Setup(scope => scope.ServiceProvider).Returns(mockServiceProvider.Object); @@ -141,7 +206,10 @@ public TestMissionEventHandler(DatabaseFixture fixture) mockFactory.Setup(f => f.CreateScope()).Returns(mockScope.Object); // Instantiating the event handlers are required for the event subscribers to be activated - _missionEventHandler = new MissionEventHandler(missionEventHandlerLogger, mockFactory.Object); + _missionEventHandler = new MissionEventHandler( + missionEventHandlerLogger, + mockFactory.Object + ); _mqttEventHandler = new MqttEventHandler(mqttEventHandlerLogger, mockFactory.Object); } @@ -157,16 +225,30 @@ public async Task ScheduledMissionStartedWhenSystemIsAvailable() // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); - var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, deck); - var missionRun = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); + var robot = await _databaseUtilities.NewRobot( + RobotStatus.Available, + installation, + deck + ); + var missionRun = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck + ); // Act await _missionRunService.Create(missionRun); Thread.Sleep(100); // Assert - var postTestMissionRun = await _missionRunService.ReadById(missionRun.Id, readOnly: true); + var postTestMissionRun = await _missionRunService.ReadById( + missionRun.Id, + readOnly: true + ); Assert.Equal(MissionStatus.Ongoing, postTestMissionRun!.Status); } @@ -176,10 +258,25 @@ public async Task SecondScheduledMissionQueuedIfRobotIsBusy() // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); - var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, deck); - var missionRunOne = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck); - var missionRunTwo = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); + var robot = await _databaseUtilities.NewRobot( + RobotStatus.Available, + installation, + deck + ); + var missionRunOne = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck + ); + var missionRunTwo = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck + ); // Act await _missionRunService.Create(missionRunOne); @@ -187,8 +284,14 @@ public async Task SecondScheduledMissionQueuedIfRobotIsBusy() await _missionRunService.Create(missionRunTwo); // Assert - var postTestMissionRunOne = await _missionRunService.ReadById(missionRunOne.Id, readOnly: true); - var postTestMissionRunTwo = await _missionRunService.ReadById(missionRunTwo.Id, readOnly: true); + var postTestMissionRunOne = await _missionRunService.ReadById( + missionRunOne.Id, + readOnly: true + ); + var postTestMissionRunTwo = await _missionRunService.ReadById( + missionRunTwo.Id, + readOnly: true + ); Assert.Equal(MissionStatus.Ongoing, postTestMissionRunOne!.Status); Assert.Equal(MissionStatus.Pending, postTestMissionRunTwo!.Status); } @@ -199,9 +302,16 @@ public async Task NewMissionIsStartedWhenRobotBecomesAvailable() // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); var robot = await _databaseUtilities.NewRobot(RobotStatus.Busy, installation, deck); - var missionRun = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck); + var missionRun = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck + ); await _missionRunService.Create(missionRun); Thread.Sleep(100); @@ -212,15 +322,19 @@ public async Task NewMissionIsStartedWhenRobotBecomesAvailable() RobotName = robot.Name, IsarId = robot.IsarId, Status = RobotStatus.Available, - Timestamp = DateTime.UtcNow - }); + Timestamp = DateTime.UtcNow, + } + ); // Act _mqttService.RaiseEvent(nameof(MqttService.MqttIsarStatusReceived), mqttEventArgs); Thread.Sleep(500); // Assert - var postTestMissionRun = await _missionRunService.ReadById(missionRun.Id, readOnly: true); + var postTestMissionRun = await _missionRunService.ReadById( + missionRun.Id, + readOnly: true + ); Assert.Equal(MissionStatus.Ongoing, postTestMissionRun!.Status); } @@ -230,7 +344,10 @@ public async Task ReturnToHomeMissionIsStartedIfQueueIsEmptyWhenRobotBecomesAvai // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); var robot = await _databaseUtilities.NewRobot(RobotStatus.Busy, installation, deck); var mqttEventArgs = new MqttReceivedArgs( @@ -239,8 +356,9 @@ public async Task ReturnToHomeMissionIsStartedIfQueueIsEmptyWhenRobotBecomesAvai RobotName = robot.Name, IsarId = robot.IsarId, Status = RobotStatus.Available, - Timestamp = DateTime.UtcNow - }); + Timestamp = DateTime.UtcNow, + } + ); // Act _mqttService.RaiseEvent(nameof(MqttService.MqttIsarStatusReceived), mqttEventArgs); @@ -250,12 +368,12 @@ public async Task ReturnToHomeMissionIsStartedIfQueueIsEmptyWhenRobotBecomesAvai var ongoingMission = await _missionRunService.ReadAll( new MissionRunQueryStringParameters { - Statuses = [ - MissionStatus.Ongoing - ], + Statuses = [MissionStatus.Ongoing], OrderBy = "DesiredStartTime", - PageSize = 100 - }, readOnly: true); + PageSize = 100, + }, + readOnly: true + ); Assert.True(ongoingMission.Any()); } @@ -265,7 +383,10 @@ public async Task ReturnToHomeMissionIsNotStartedIfReturnToHomeIsNotSupported() // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); var robot = await _databaseUtilities.NewRobot(RobotStatus.Busy, installation, deck); robot.RobotCapabilities!.Remove(RobotCapabilitiesEnum.return_to_home); await _robotService.Update(robot); @@ -276,8 +397,9 @@ public async Task ReturnToHomeMissionIsNotStartedIfReturnToHomeIsNotSupported() RobotName = robot.Name, IsarId = robot.IsarId, Status = RobotStatus.Available, - Timestamp = DateTime.UtcNow - }); + Timestamp = DateTime.UtcNow, + } + ); // Act _mqttService.RaiseEvent(nameof(MqttService.MqttIsarStatusReceived), mqttEventArgs); @@ -287,12 +409,12 @@ public async Task ReturnToHomeMissionIsNotStartedIfReturnToHomeIsNotSupported() var ongoingMission = await _missionRunService.ReadAll( new MissionRunQueryStringParameters { - Statuses = [ - MissionStatus.Ongoing - ], + Statuses = [MissionStatus.Ongoing], OrderBy = "DesiredStartTime", - PageSize = 100 - }, readOnly: true); + PageSize = 100, + }, + readOnly: true + ); Assert.False(ongoingMission.Any()); } @@ -302,18 +424,40 @@ public async Task MissionRunIsStartedForOtherAvailableRobotIfOneRobotHasAnOngoin // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); - var robotOne = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, deck); - var robotTwo = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, deck); - var missionRunOne = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robotOne, deck); - var missionRunTwo = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robotTwo, deck); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); + var robotOne = await _databaseUtilities.NewRobot( + RobotStatus.Available, + installation, + deck + ); + var robotTwo = await _databaseUtilities.NewRobot( + RobotStatus.Available, + installation, + deck + ); + var missionRunOne = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robotOne, + deck + ); + var missionRunTwo = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robotTwo, + deck + ); // Act (Ensure first mission is started) await _missionRunService.Create(missionRunOne); Thread.Sleep(100); // Assert - var postStartMissionRunOne = await _missionRunService.ReadById(missionRunOne.Id, readOnly: true); + var postStartMissionRunOne = await _missionRunService.ReadById( + missionRunOne.Id, + readOnly: true + ); Assert.NotNull(postStartMissionRunOne); Assert.Equal(MissionStatus.Ongoing, postStartMissionRunOne.Status); @@ -322,7 +466,10 @@ public async Task MissionRunIsStartedForOtherAvailableRobotIfOneRobotHasAnOngoin Thread.Sleep(100); // Assert - var postStartMissionRunTwo = await _missionRunService.ReadById(missionRunTwo.Id, readOnly: true); + var postStartMissionRunTwo = await _missionRunService.ReadById( + missionRunTwo.Id, + readOnly: true + ); Assert.NotNull(postStartMissionRunTwo); Assert.Equal(MissionStatus.Ongoing, postStartMissionRunTwo.Status); } @@ -333,18 +480,41 @@ public async Task QueuedMissionsAreNotAbortedWhenRobotAvailableHappensAtTheSameT // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); - var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, null); - var missionRun1 = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck, true); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); + var robot = await _databaseUtilities.NewRobot( + RobotStatus.Available, + installation, + null + ); + var missionRun1 = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck, + true + ); Thread.Sleep(100); - var missionRun2 = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck, true); + var missionRun2 = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck, + true + ); Thread.Sleep(100); var missionRunCreatedEventArgs = new MissionRunCreatedEventArgs(missionRun1.Id); - _missionRunService.RaiseEvent(nameof(MissionRunService.MissionRunCreated), missionRunCreatedEventArgs); + _missionRunService.RaiseEvent( + nameof(MissionRunService.MissionRunCreated), + missionRunCreatedEventArgs + ); Thread.Sleep(100); - var missionRun1PostCreation = await _missionRunService.ReadById(missionRun1.Id, readOnly: true); + var missionRun1PostCreation = await _missionRunService.ReadById( + missionRun1.Id, + readOnly: true + ); Assert.NotNull(missionRun1PostCreation); // Act @@ -355,19 +525,32 @@ public async Task QueuedMissionsAreNotAbortedWhenRobotAvailableHappensAtTheSameT IsarId = robot.IsarId, MissionId = missionRun1PostCreation.IsarMissionId, Status = "successful", - Timestamp = DateTime.UtcNow - }); + Timestamp = DateTime.UtcNow, + } + ); var robotAvailableEventArgs = new RobotAvailableEventArgs(robot.Id); - _mqttService.RaiseEvent(nameof(MqttService.MqttIsarMissionReceived), mqttIsarMissionEventArgs); - _missionSchedulingService.RaiseEvent(nameof(MissionSchedulingService.RobotAvailable), robotAvailableEventArgs); + _mqttService.RaiseEvent( + nameof(MqttService.MqttIsarMissionReceived), + mqttIsarMissionEventArgs + ); + _missionSchedulingService.RaiseEvent( + nameof(MissionSchedulingService.RobotAvailable), + robotAvailableEventArgs + ); Thread.Sleep(500); // Assert - var postTestMissionRun1 = await _missionRunService.ReadById(missionRun1.Id, readOnly: true); + var postTestMissionRun1 = await _missionRunService.ReadById( + missionRun1.Id, + readOnly: true + ); Assert.Equal(MissionStatus.Successful, postTestMissionRun1!.Status); - var postTestMissionRun2 = await _missionRunService.ReadById(missionRun2.Id, readOnly: true); + var postTestMissionRun2 = await _missionRunService.ReadById( + missionRun2.Id, + readOnly: true + ); Assert.Equal(MissionStatus.Pending, postTestMissionRun2!.Status); } @@ -379,14 +562,37 @@ public async Task QueuedContinuesWhenOnIsarStatusHappensAtTheSameTimeAsOnIsarMis // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); - var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, deck); - var missionRun1 = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck, true, MissionRunType.Normal, MissionStatus.Ongoing, Guid.NewGuid().ToString()); - var missionRun2 = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck, true); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); + var robot = await _databaseUtilities.NewRobot( + RobotStatus.Available, + installation, + deck + ); + var missionRun1 = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck, + true, + MissionRunType.Normal, + MissionStatus.Ongoing, + Guid.NewGuid().ToString() + ); + var missionRun2 = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck, + true + ); Thread.Sleep(100); var missionRunCreatedEventArgs = new MissionRunCreatedEventArgs(missionRun1.Id); - _missionRunService.RaiseEvent(nameof(MissionRunService.MissionRunCreated), missionRunCreatedEventArgs); + _missionRunService.RaiseEvent( + nameof(MissionRunService.MissionRunCreated), + missionRunCreatedEventArgs + ); Thread.Sleep(100); // Act @@ -397,8 +603,9 @@ public async Task QueuedContinuesWhenOnIsarStatusHappensAtTheSameTimeAsOnIsarMis IsarId = robot.IsarId, MissionId = missionRun1.IsarMissionId, Status = "successful", - Timestamp = DateTime.UtcNow - }); + Timestamp = DateTime.UtcNow, + } + ); var mqttIsarStatusEventArgs = new MqttReceivedArgs( new IsarStatusMessage @@ -406,17 +613,33 @@ public async Task QueuedContinuesWhenOnIsarStatusHappensAtTheSameTimeAsOnIsarMis RobotName = robot.Name, IsarId = robot.IsarId, Status = RobotStatus.Available, - Timestamp = DateTime.UtcNow - }); - - _mqttService.RaiseEvent(nameof(MqttService.MqttIsarMissionReceived), mqttIsarMissionEventArgs); - _mqttService.RaiseEvent(nameof(MqttService.MqttIsarStatusReceived), mqttIsarStatusEventArgs); + Timestamp = DateTime.UtcNow, + } + ); + + _mqttService.RaiseEvent( + nameof(MqttService.MqttIsarMissionReceived), + mqttIsarMissionEventArgs + ); + _mqttService.RaiseEvent( + nameof(MqttService.MqttIsarStatusReceived), + mqttIsarStatusEventArgs + ); Thread.Sleep(2500); // Accommodate for sleep in OnIsarStatus // Assert - var postTestMissionRun1 = await _missionRunService.ReadById(missionRun1.Id, readOnly: true); - Assert.Equal(Api.Database.Models.TaskStatus.Successful, postTestMissionRun1!.Tasks[0].Status); - var postTestMissionRun2 = await _missionRunService.ReadById(missionRun2.Id, readOnly: true); + var postTestMissionRun1 = await _missionRunService.ReadById( + missionRun1.Id, + readOnly: true + ); + Assert.Equal( + Api.Database.Models.TaskStatus.Successful, + postTestMissionRun1!.Tasks[0].Status + ); + var postTestMissionRun2 = await _missionRunService.ReadById( + missionRun2.Id, + readOnly: true + ); Assert.Equal(MissionStatus.Ongoing, postTestMissionRun2!.Status); } @@ -426,10 +649,29 @@ public async Task ReturnHomeMissionAbortedIfNewMissionScheduled() // Arrange var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); var robot = await _databaseUtilities.NewRobot(RobotStatus.Busy, installation, deck); - var returnToHomeMission = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck, true, MissionRunType.ReturnHome, MissionStatus.Ongoing, Guid.NewGuid().ToString()); - var missionRun = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck, true, MissionRunType.Normal, MissionStatus.Pending, Guid.NewGuid().ToString()); + var returnToHomeMission = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck, + true, + MissionRunType.ReturnHome, + MissionStatus.Ongoing, + Guid.NewGuid().ToString() + ); + var missionRun = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck, + true, + MissionRunType.Normal, + MissionStatus.Pending, + Guid.NewGuid().ToString() + ); Thread.Sleep(100); @@ -439,7 +681,10 @@ public async Task ReturnHomeMissionAbortedIfNewMissionScheduled() Thread.Sleep(500); // Assert - var updatedReturnHomeMission = await _missionRunService.ReadById(returnToHomeMission.Id, readOnly: true); + var updatedReturnHomeMission = await _missionRunService.ReadById( + returnToHomeMission.Id, + readOnly: true + ); Assert.True(updatedReturnHomeMission?.Status.Equals(MissionStatus.Aborted)); // Act @@ -450,8 +695,9 @@ public async Task ReturnHomeMissionAbortedIfNewMissionScheduled() IsarId = robot.IsarId, MissionId = returnToHomeMission.IsarMissionId, Status = "cancelled", - Timestamp = DateTime.UtcNow - }); + Timestamp = DateTime.UtcNow, + } + ); var mqttIsarStatusEventArgs = new MqttReceivedArgs( new IsarStatusMessage @@ -459,14 +705,24 @@ public async Task ReturnHomeMissionAbortedIfNewMissionScheduled() RobotName = robot.Name, IsarId = robot.IsarId, Status = RobotStatus.Available, - Timestamp = DateTime.UtcNow - }); - - _mqttService.RaiseEvent(nameof(MqttService.MqttIsarMissionReceived), mqttIsarMissionEventArgs); - _mqttService.RaiseEvent(nameof(MqttService.MqttIsarStatusReceived), mqttIsarStatusEventArgs); + Timestamp = DateTime.UtcNow, + } + ); + + _mqttService.RaiseEvent( + nameof(MqttService.MqttIsarMissionReceived), + mqttIsarMissionEventArgs + ); + _mqttService.RaiseEvent( + nameof(MqttService.MqttIsarStatusReceived), + mqttIsarStatusEventArgs + ); Thread.Sleep(500); - var updatedMissionRun = await _missionRunService.ReadById(missionRun.Id, readOnly: true); + var updatedMissionRun = await _missionRunService.ReadById( + missionRun.Id, + readOnly: true + ); Assert.True(updatedMissionRun?.Status.Equals(MissionStatus.Ongoing)); } } diff --git a/backend/api.test/Mocks/BlobServiceMock.cs b/backend/api.test/Mocks/BlobServiceMock.cs index ded5b749c..2ad953b7c 100644 --- a/backend/api.test/Mocks/BlobServiceMock.cs +++ b/backend/api.test/Mocks/BlobServiceMock.cs @@ -7,7 +7,11 @@ namespace Api.Test.Mocks { public class MockBlobService : IBlobService { - public async Task DownloadBlob(string blobName, string containerName, string accountName) + public async Task DownloadBlob( + string blobName, + string containerName, + string accountName + ) { using var memoryStream = new System.IO.MemoryStream(); await Task.CompletedTask; @@ -21,7 +25,13 @@ public AsyncPageable FetchAllBlobs(string containerName, string accoun return pages; } - public async Task UploadJsonToBlob(string json, string path, string containerName, string accountName, bool overwrite = false) + public async Task UploadJsonToBlob( + string json, + string path, + string containerName, + string accountName, + bool overwrite = false + ) { await Task.CompletedTask; } diff --git a/backend/api.test/Mocks/HttpContextAccessorMock.cs b/backend/api.test/Mocks/HttpContextAccessorMock.cs index a61ec962f..d882e16ff 100644 --- a/backend/api.test/Mocks/HttpContextAccessorMock.cs +++ b/backend/api.test/Mocks/HttpContextAccessorMock.cs @@ -25,10 +25,22 @@ private static HttpContext GetHttpContextWithRoles(List roles) byte[] key = new byte[32]; rng.GetBytes(key); var securityKey = new SymmetricSecurityKey(key) { KeyId = Guid.NewGuid().ToString() }; - var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); + var signingCredentials = new SigningCredentials( + securityKey, + SecurityAlgorithms.HmacSha256 + ); string issuer = Guid.NewGuid().ToString(); - string jwtToken = tokenHandler.WriteToken(new JwtSecurityToken(issuer, null, claims, null, DateTime.UtcNow.AddMinutes(20), signingCredentials)); + string jwtToken = tokenHandler.WriteToken( + new JwtSecurityToken( + issuer, + null, + claims, + null, + DateTime.UtcNow.AddMinutes(20), + signingCredentials + ) + ); context.Request.Headers.Authorization = jwtToken; return context; } @@ -42,7 +54,8 @@ public HttpContext? HttpContext { get { - if (CustomRolesHttpContext is not null) return CustomRolesHttpContext; + if (CustomRolesHttpContext is not null) + return CustomRolesHttpContext; return GetHttpContextWithRoles(["Role.Admin"]); } diff --git a/backend/api.test/Mocks/IsarServiceMock.cs b/backend/api.test/Mocks/IsarServiceMock.cs index 9bd3d797a..15fe3c0ff 100644 --- a/backend/api.test/Mocks/IsarServiceMock.cs +++ b/backend/api.test/Mocks/IsarServiceMock.cs @@ -15,7 +15,7 @@ public async Task StartMission(Robot robot, MissionRun mission) new IsarStartMissionResponse { MissionId = System.Guid.NewGuid().ToString(), - Tasks = [] + Tasks = [], } ); return isarServiceMissionResponse; @@ -43,11 +43,7 @@ public async Task StartMoveArm(Robot robot, string position) { await Task.Run(() => Thread.Sleep(1)); var isarServiceMissionResponse = new IsarMission( - new IsarStartMissionResponse - { - MissionId = "testStartMoveArm", - Tasks = [] - } + new IsarStartMissionResponse { MissionId = "testStartMoveArm", Tasks = [] } ); return isarServiceMissionResponse; } @@ -60,7 +56,7 @@ public async Task GetMediaStreamConfig(Robot robot) Url = "mockURL", Token = "mockToken", RobotId = robot.Id, - MediaConnectionType = MediaConnectionType.LiveKit + MediaConnectionType = MediaConnectionType.LiveKit, }; } } diff --git a/backend/api.test/Mocks/MapServiceMock.cs b/backend/api.test/Mocks/MapServiceMock.cs index 38cc24dd1..9f7516adb 100644 --- a/backend/api.test/Mocks/MapServiceMock.cs +++ b/backend/api.test/Mocks/MapServiceMock.cs @@ -9,7 +9,10 @@ namespace Api.Test.Mocks { public class MockMapService : IMapService { - public async Task ChooseMapFromPositions(IList positions, string installationCode) + public async Task ChooseMapFromPositions( + IList positions, + string installationCode + ) { await Task.Run(() => Thread.Sleep(1)); return new MapMetadata(); diff --git a/backend/api.test/Mocks/MissionLoaderMock.cs b/backend/api.test/Mocks/MissionLoaderMock.cs index 9acaf350b..6b9fef25e 100644 --- a/backend/api.test/Mocks/MissionLoaderMock.cs +++ b/backend/api.test/Mocks/MissionLoaderMock.cs @@ -6,24 +6,23 @@ using Api.Controllers.Models; using Api.Database.Models; using Api.Services.MissionLoaders; + namespace Api.Test.Mocks { public class MockMissionLoader() : IMissionLoader { - private readonly List _mockPlantInfo = [ + private readonly List _mockPlantInfo = + [ new PlantInfo { PlantCode = "testInstallation", - ProjectDescription = "testInstallation" + ProjectDescription = "testInstallation", }, - new PlantInfo - { - PlantCode = "JSV", - ProjectDescription = "JSVtestInstallation" - } + new PlantInfo { PlantCode = "JSV", ProjectDescription = "JSVtestInstallation" }, ]; - private readonly List _mockMissionTasks = [ + private readonly List _mockMissionTasks = + [ new MissionTask( inspection: new Inspection(), taskOrder: 0, @@ -33,8 +32,19 @@ public class MockMissionLoader() : IMissionLoader taskDescription: "description", robotPose: new Pose { - Position = new Position { X = 0, Y = 0, Z = 0 }, - Orientation = new Orientation { X = 0, Y = 0, Z = 0, W = 1 } + Position = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + Orientation = new Orientation + { + X = 0, + Y = 0, + Z = 0, + W = 1, + }, } ), new MissionTask( @@ -46,22 +56,34 @@ public class MockMissionLoader() : IMissionLoader taskDescription: "description", robotPose: new Pose { - Position = new Position { X = 0, Y = 0, Z = 0 }, - Orientation = new Orientation { X = 0, Y = 0, Z = 0, W = 1 } + Position = new Position + { + X = 0, + Y = 0, + Z = 0, + }, + Orientation = new Orientation + { + X = 0, + Y = 0, + Z = 0, + W = 1, + }, } ), ]; - private readonly MissionDefinition _mockMissionDefinition = new() - { - InspectionArea = new Deck(), - Comment = "", - Id = "", - InstallationCode = "TTT", - IsDeprecated = false, - Name = "test", - Source = new Source { Id = "", SourceId = "" } - }; + private readonly MissionDefinition _mockMissionDefinition = + new() + { + InspectionArea = new Deck(), + Comment = "", + Id = "", + InstallationCode = "TTT", + IsDeprecated = false, + Name = "test", + Source = new Source { Id = "", SourceId = "" }, + }; public async Task GetMissionById(string sourceMissionId) { @@ -69,7 +91,9 @@ public class MockMissionLoader() : IMissionLoader return _mockMissionDefinition; } - public async Task> GetAvailableMissions(string? installationCode) + public async Task> GetAvailableMissions( + string? installationCode + ) { await Task.Run(() => Thread.Sleep(1)); return new List([_mockMissionDefinition]).AsQueryable(); diff --git a/backend/api.test/Mocks/RobotControllerMock.cs b/backend/api.test/Mocks/RobotControllerMock.cs index 513cc4439..cb3338e97 100644 --- a/backend/api.test/Mocks/RobotControllerMock.cs +++ b/backend/api.test/Mocks/RobotControllerMock.cs @@ -2,6 +2,7 @@ using Api.Services; using Microsoft.Extensions.Logging; using Moq; + namespace Api.Test.Mocks { internal class RobotControllerMock @@ -38,7 +39,7 @@ public RobotControllerMock() ErrorHandlingServiceMock.Object ) { - CallBase = true + CallBase = true, }; } } diff --git a/backend/api.test/Mocks/SignalRServiceMock.cs b/backend/api.test/Mocks/SignalRServiceMock.cs index 25acf3e4c..185d7da82 100644 --- a/backend/api.test/Mocks/SignalRServiceMock.cs +++ b/backend/api.test/Mocks/SignalRServiceMock.cs @@ -1,14 +1,17 @@ using System.Threading.Tasks; using Api.Database.Models; + namespace Api.Services { public class MockSignalRService : ISignalRService { - public MockSignalRService() - { - } + public MockSignalRService() { } - public async Task SendMessageAsync(string label, Installation? installation, T messageObject) + public async Task SendMessageAsync( + string label, + Installation? installation, + T messageObject + ) { await Task.CompletedTask; } @@ -23,7 +26,6 @@ public void ReportDockFailureToSignalR(Robot robot, string message) return; } - public void ReportDockSuccessToSignalR(Robot robot, string message) { return; diff --git a/backend/api.test/Mocks/StidServiceMock.cs b/backend/api.test/Mocks/StidServiceMock.cs index 802f8b2d3..49310994e 100644 --- a/backend/api.test/Mocks/StidServiceMock.cs +++ b/backend/api.test/Mocks/StidServiceMock.cs @@ -16,19 +16,28 @@ public class MockStidService(FlotillaDbContext context) : IStidService await Task.CompletedTask; string testAreaName = "StidServiceMockArea"; - var area = context.Areas - .Include(a => a.Deck).ThenInclude(d => d.Installation) - .Include(a => a.Deck).ThenInclude(d => d.Plant).ThenInclude(p => p.Installation) + var area = context + .Areas.Include(a => a.Deck) + .ThenInclude(d => d.Installation) + .Include(a => a.Deck) + .ThenInclude(d => d.Plant) + .ThenInclude(p => p.Installation) .Include(d => d.Plant) - .Include(i => i.Installation).Include(d => d.DefaultLocalizationPose) - .Where(area => area.Name.Contains(testAreaName)).ToList().FirstOrDefault(); - if (area != null) { return area; } + .Include(i => i.Installation) + .Include(d => d.DefaultLocalizationPose) + .Where(area => area.Name.Contains(testAreaName)) + .ToList() + .FirstOrDefault(); + if (area != null) + { + return area; + } var testInstallation = new Installation { Id = Guid.NewGuid().ToString(), Name = "StidServiceMockInstallation", - InstallationCode = "TTT" + InstallationCode = "TTT", }; var testPlant = new Plant @@ -36,7 +45,7 @@ public class MockStidService(FlotillaDbContext context) : IStidService Id = Guid.NewGuid().ToString(), Installation = testInstallation, Name = "StidServiceMockPlant", - PlantCode = "TTT" + PlantCode = "TTT", }; var testDeck = new Deck @@ -45,7 +54,7 @@ public class MockStidService(FlotillaDbContext context) : IStidService Plant = testPlant, Installation = testPlant.Installation, DefaultLocalizationPose = new DefaultLocalizationPose(), - Name = "StidServiceMockDeck" + Name = "StidServiceMockDeck", }; var testArea = new Area diff --git a/backend/api.test/Services/MissionService.cs b/backend/api.test/Services/MissionService.cs index 9bc307239..a1670d99f 100644 --- a/backend/api.test/Services/MissionService.cs +++ b/backend/api.test/Services/MissionService.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using Moq; using Xunit; + namespace Api.Test.Services { [Collection("Database collection")] @@ -35,14 +36,45 @@ public MissionServiceTest(DatabaseFixture fixture) _logger = new Mock>().Object; _signalRService = new MockSignalRService(); _accessRoleService = new AccessRoleService(_context, new HttpContextAccessor()); - _userInfoService = new UserInfoService(_context, new HttpContextAccessor(), new Mock>().Object); - _missionTaskService = new MissionTaskService(_context, new Mock>().Object); + _userInfoService = new UserInfoService( + _context, + new HttpContextAccessor(), + new Mock>().Object + ); + _missionTaskService = new MissionTaskService( + _context, + new Mock>().Object + ); _installationService = new InstallationService(_context, _accessRoleService); _plantService = new PlantService(_context, _installationService, _accessRoleService); - _deckService = new DeckService(_context, defaultLocalizationPoseService, _installationService, _plantService, _accessRoleService, new MockSignalRService()); + _deckService = new DeckService( + _context, + defaultLocalizationPoseService, + _installationService, + _plantService, + _accessRoleService, + new MockSignalRService() + ); _robotModelService = new RobotModelService(_context); - _robotService = new RobotService(_context, new Mock>().Object, _robotModelService, new MockSignalRService(), _accessRoleService, _installationService, _deckService); - _missionRunService = new MissionRunService(_context, _signalRService, _logger, _accessRoleService, _missionTaskService, _deckService, _robotService, _userInfoService); + _robotService = new RobotService( + _context, + new Mock>().Object, + _robotModelService, + new MockSignalRService(), + _accessRoleService, + _installationService, + _deckService + ); + _missionRunService = new MissionRunService( + _context, + _signalRService, + _logger, + _accessRoleService, + _missionTaskService, + _deckService, + _robotService, + _userInfoService + ); _databaseUtilities = new DatabaseUtilities(_context); } @@ -55,7 +87,10 @@ public void Dispose() [Fact] public async Task ReadIdDoesNotExist() { - var missionRun = await _missionRunService.ReadById("some_id_that_does_not_exist", readOnly: true); + var missionRun = await _missionRunService.ReadById( + "some_id_that_does_not_exist", + readOnly: true + ); Assert.Null(missionRun); } @@ -70,9 +105,16 @@ public async Task Create() var installation = await _databaseUtilities.ReadOrNewInstallation(); var plant = await _databaseUtilities.ReadOrNewPlant(installation.InstallationCode); - var deck = await _databaseUtilities.ReadOrNewDeck(installation.InstallationCode, plant.PlantCode); + var deck = await _databaseUtilities.ReadOrNewDeck( + installation.InstallationCode, + plant.PlantCode + ); var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); - var missionRun = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, deck); + var missionRun = await _databaseUtilities.NewMissionRun( + installation.InstallationCode, + robot, + deck + ); await _missionRunService.Create(missionRun); diff --git a/backend/api.test/Services/RobotService.cs b/backend/api.test/Services/RobotService.cs index 41fd5f61a..3cbe29e65 100644 --- a/backend/api.test/Services/RobotService.cs +++ b/backend/api.test/Services/RobotService.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging; using Moq; using Xunit; + namespace Api.Test.Services { [Collection("Database collection")] @@ -38,8 +39,22 @@ public RobotServiceTest(DatabaseFixture fixture) _installationService = new InstallationService(_context, _accessRoleService); _plantService = new PlantService(_context, _installationService, _accessRoleService); _defaultLocalizationPoseService = new DefaultLocalizationPoseService(_context); - _deckService = new DeckService(_context, _defaultLocalizationPoseService, _installationService, _plantService, _accessRoleService, _signalRService); - _areaService = new AreaService(_context, _installationService, _plantService, _deckService, _defaultLocalizationPoseService, _accessRoleService); + _deckService = new DeckService( + _context, + _defaultLocalizationPoseService, + _installationService, + _plantService, + _accessRoleService, + _signalRService + ); + _areaService = new AreaService( + _context, + _installationService, + _plantService, + _deckService, + _defaultLocalizationPoseService, + _accessRoleService + ); } public void Dispose() @@ -53,7 +68,15 @@ public async Task ReadAll() { var installation = await _databaseUtilities.ReadOrNewInstallation(); var _ = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _deckService); + var robotService = new RobotService( + _context, + _logger, + _robotModelService, + _signalRService, + _accessRoleService, + _installationService, + _deckService + ); var robots = await robotService.ReadAll(); Assert.True(robots.Any()); @@ -62,7 +85,15 @@ public async Task ReadAll() [Fact] public async Task Read() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _deckService); + var robotService = new RobotService( + _context, + _logger, + _robotModelService, + _signalRService, + _accessRoleService, + _installationService, + _deckService + ); var installation = await _databaseUtilities.ReadOrNewInstallation(); var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); var robotById = await robotService.ReadById(robot.Id, readOnly: false); @@ -73,7 +104,15 @@ public async Task Read() [Fact] public async Task ReadIdDoesNotExist() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _deckService); + var robotService = new RobotService( + _context, + _logger, + _robotModelService, + _signalRService, + _accessRoleService, + _installationService, + _deckService + ); var robot = await robotService.ReadById("some_id_that_does_not_exist", readOnly: true); Assert.Null(robot); } @@ -81,14 +120,20 @@ public async Task ReadIdDoesNotExist() [Fact] public async Task Create() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _deckService); + var robotService = new RobotService( + _context, + _logger, + _robotModelService, + _signalRService, + _accessRoleService, + _installationService, + _deckService + ); var installationService = new InstallationService(_context, _accessRoleService); - var installation = await installationService.Create(new CreateInstallationQuery - { - Name = "Johan Sverdrup", - InstallationCode = "JSV" - }); + var installation = await installationService.Create( + new CreateInstallationQuery { Name = "Johan Sverdrup", InstallationCode = "JSV" } + ); var robotsBefore = await robotService.ReadAll(readOnly: true); int nRobotsBefore = robotsBefore.Count(); @@ -102,15 +147,12 @@ public async Task Create() Name = "", IsarId = "", SerialNumber = "", - Documentation = - [ - documentationQuery - ], + Documentation = [documentationQuery], CurrentInstallationCode = installation.InstallationCode, RobotType = RobotType.Robot, Host = "", Port = 1, - Status = RobotStatus.Available + Status = RobotStatus.Available, }; var robotModel = _context.RobotModels.First(); diff --git a/backend/api.test/TestWebApplicationFactory.cs b/backend/api.test/TestWebApplicationFactory.cs index 4b6861ebf..53587c711 100644 --- a/backend/api.test/TestWebApplicationFactory.cs +++ b/backend/api.test/TestWebApplicationFactory.cs @@ -9,17 +9,17 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; + namespace Api.Test { - public class TestWebApplicationFactory : WebApplicationFactory where TProgram : class + public class TestWebApplicationFactory : WebApplicationFactory + where TProgram : class { protected override void ConfigureWebHost(IWebHostBuilder builder) { string projectDir = Directory.GetCurrentDirectory(); string configPath = Path.Combine(projectDir, "appsettings.Test.json"); - var configuration = new ConfigurationBuilder() - .AddJsonFile(configPath) - .Build(); + var configuration = new ConfigurationBuilder().AddJsonFile(configPath).Build(); builder.UseEnvironment("Test"); builder.ConfigureAppConfiguration( (context, config) => @@ -27,35 +27,32 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) config.AddJsonFile(configPath).AddEnvironmentVariables(); } ); - builder.ConfigureTestServices( - services => - { - services.AddScoped(); - services.AddScoped(); - services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddAuthorizationBuilder().AddFallbackPolicy( - TestAuthHandler.AuthenticationScheme, policy => policy.RequireAuthenticatedUser() + builder.ConfigureTestServices(services => + { + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services + .AddAuthorizationBuilder() + .AddFallbackPolicy( + TestAuthHandler.AuthenticationScheme, + policy => policy.RequireAuthenticatedUser() ); - services - .AddAuthentication( - options => - { - options.DefaultAuthenticateScheme = - TestAuthHandler.AuthenticationScheme; - options.DefaultChallengeScheme = - TestAuthHandler.AuthenticationScheme; - } - ) - .AddScheme( - TestAuthHandler.AuthenticationScheme, - options => { } - ); - } - ); + services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = TestAuthHandler.AuthenticationScheme; + options.DefaultChallengeScheme = TestAuthHandler.AuthenticationScheme; + }) + .AddScheme( + TestAuthHandler.AuthenticationScheme, + options => { } + ); + }); } } } diff --git a/backend/api/Configurations/ConfigurationBuilderExtensions.cs b/backend/api/Configurations/ConfigurationBuilderExtensions.cs index fdcc6c065..633edd9c6 100644 --- a/backend/api/Configurations/ConfigurationBuilderExtensions.cs +++ b/backend/api/Configurations/ConfigurationBuilderExtensions.cs @@ -11,8 +11,8 @@ public static class ConfigurationBuilderExtensions /// public static void AddAppSettingsEnvironmentVariables(this WebApplicationBuilder builder) { - string? clientId = builder.Configuration - .GetSection("AzureAd") + string? clientId = builder + .Configuration.GetSection("AzureAd") .GetValue("ClientId"); if (clientId is not null) { @@ -20,8 +20,8 @@ public static void AddAppSettingsEnvironmentVariables(this WebApplicationBuilder Console.WriteLine("'AZURE_CLIENT_ID' set to " + clientId); } - string? tenantId = builder.Configuration - .GetSection("AzureAd") + string? tenantId = builder + .Configuration.GetSection("AzureAd") .GetValue("TenantId"); if (tenantId is not null) { @@ -31,10 +31,9 @@ public static void AddAppSettingsEnvironmentVariables(this WebApplicationBuilder if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Local") { - - string? userId = builder.Configuration - .GetSection("Local") - .GetValue("DevUserId"); + string? userId = builder + .Configuration.GetSection("Local") + .GetValue("DevUserId"); if (tenantId is not null) { Environment.SetEnvironmentVariable("LOCAL_DEVUSERID", userId); @@ -51,15 +50,17 @@ public static void AddAppSettingsEnvironmentVariables(this WebApplicationBuilder /// /// /// - public static void AddDotEnvironmentVariables(this WebApplicationBuilder builder, string filePath) + public static void AddDotEnvironmentVariables( + this WebApplicationBuilder builder, + string filePath + ) { - if (!File.Exists(filePath)) return; + if (!File.Exists(filePath)) + return; foreach (string line in File.ReadAllLines(filePath)) { - string[] parts = line.Split( - '=', - StringSplitOptions.RemoveEmptyEntries); + string[] parts = line.Split('=', StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0 || parts[0].StartsWith('#')) continue; @@ -77,19 +78,17 @@ public static void AddDotEnvironmentVariables(this WebApplicationBuilder builder /// public static void ConfigureLogger(this WebApplicationBuilder builder) { - builder.Logging.AddSimpleConsole( - options => - { - options.IncludeScopes = true; - options.TimestampFormat = "yyyy-MM-dd HH:mm:ss - "; - options.ColorBehavior = Microsoft - .Extensions - .Logging - .Console - .LoggerColorBehavior - .Enabled; - } - ); + builder.Logging.AddSimpleConsole(options => + { + options.IncludeScopes = true; + options.TimestampFormat = "yyyy-MM-dd HH:mm:ss - "; + options.ColorBehavior = Microsoft + .Extensions + .Logging + .Console + .LoggerColorBehavior + .Enabled; + }); } } } diff --git a/backend/api/Configurations/CustomServiceConfigurations.cs b/backend/api/Configurations/CustomServiceConfigurations.cs index 88b9cf93b..2e9c71550 100644 --- a/backend/api/Configurations/CustomServiceConfigurations.cs +++ b/backend/api/Configurations/CustomServiceConfigurations.cs @@ -4,6 +4,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; + namespace Api.Configurations { public static class CustomServiceConfigurations @@ -24,7 +25,7 @@ IConfiguration configuration string sqlConnectionString = new SqliteConnectionStringBuilder { DataSource = "file::memory:", - Cache = SqliteCacheMode.Shared + Cache = SqliteCacheMode.Shared, }.ToString(); // In-memory sqlite requires an open connection throughout the whole lifetime of the database @@ -34,16 +35,18 @@ IConfiguration configuration using var context = new FlotillaDbContext(dbBuilder.Options); context.Database.EnsureCreated(); - bool initializeDb = configuration.GetSection("Database").GetValue("InitializeInMemDb"); - if (initializeDb) InitDb.PopulateDb(context); + bool initializeDb = configuration + .GetSection("Database") + .GetValue("InitializeInMemDb"); + if (initializeDb) + InitDb.PopulateDb(context); // Setting splitting behavior explicitly to avoid warning - services.AddDbContext( - options => - options.UseSqlite( - sqlConnectionString, - o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery) - ) + services.AddDbContext(options => + options.UseSqlite( + sqlConnectionString, + o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery) + ) ); } else @@ -71,58 +74,58 @@ public static IServiceCollection ConfigureSwagger( IConfiguration configuration ) { - services.AddSwaggerGen( - c => - { - // Add Authorization button in UI - c.AddSecurityDefinition( - "oauth2", - new OpenApiSecurityScheme + services.AddSwaggerGen(c => + { + // Add Authorization button in UI + c.AddSecurityDefinition( + "oauth2", + new OpenApiSecurityScheme + { + Type = SecuritySchemeType.OAuth2, + Flows = new OpenApiOAuthFlows { - Type = SecuritySchemeType.OAuth2, - Flows = new OpenApiOAuthFlows + AuthorizationCode = new OpenApiOAuthFlow { - AuthorizationCode = new OpenApiOAuthFlow + TokenUrl = new Uri( + $"{configuration["AzureAd:Instance"]}/{configuration["AzureAd:TenantId"]}/oauth2/token" + ), + AuthorizationUrl = new Uri( + $"{configuration["AzureAd:Instance"]}/{configuration["AzureAd:TenantId"]}/oauth2/authorize" + ), + Scopes = new Dictionary { - TokenUrl = new Uri( - $"{configuration["AzureAd:Instance"]}/{configuration["AzureAd:TenantId"]}/oauth2/token" - ), - AuthorizationUrl = new Uri( - $"{configuration["AzureAd:Instance"]}/{configuration["AzureAd:TenantId"]}/oauth2/authorize" - ), - Scopes = new Dictionary { - { - $"api://{configuration["AzureAd:ClientId"]}/user_impersonation", "User Impersonation" - } - } - } - } - } - ); - // Show which endpoints have authorization in the UI - c.AddSecurityRequirement( - new OpenApiSecurityRequirement + $"api://{configuration["AzureAd:ClientId"]}/user_impersonation", + "User Impersonation" + }, + }, + }, + }, + } + ); + // Show which endpoints have authorization in the UI + c.AddSecurityRequirement( + new OpenApiSecurityRequirement + { { + new OpenApiSecurityScheme { - new OpenApiSecurityScheme + Reference = new OpenApiReference { - Reference = new OpenApiReference - { - Type = ReferenceType.SecurityScheme, Id = "oauth2" - } + Type = ReferenceType.SecurityScheme, + Id = "oauth2", }, - Array.Empty() - } - } - ); + }, + Array.Empty() + }, + } + ); - // Make swagger use xml comments from functions - string xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; - string xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - c.IncludeXmlComments(xmlPath); - } - ); + // Make swagger use xml comments from functions + string xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + string xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + c.IncludeXmlComments(xmlPath); + }); return services; } @@ -133,7 +136,8 @@ IConfiguration configuration ) { string? missionLoaderFileName = configuration["MissionLoader:FileName"]; - if (missionLoaderFileName == null) return services; + if (missionLoaderFileName == null) + return services; try { @@ -144,7 +148,9 @@ IConfiguration configuration } else { - throw new InvalidOperationException("The specified class does not implement IMissionLoader or could not be found."); + throw new InvalidOperationException( + "The specified class does not implement IMissionLoader or could not be found." + ); } } catch (Exception) diff --git a/backend/api/Controllers/AccessRoleController.cs b/backend/api/Controllers/AccessRoleController.cs index 6d35ebf4d..a5173ee67 100644 --- a/backend/api/Controllers/AccessRoleController.cs +++ b/backend/api/Controllers/AccessRoleController.cs @@ -9,10 +9,10 @@ namespace Api.Controllers [ApiController] [Route("access-roles")] public class AccessRoleController( - ILogger logger, - IAccessRoleService accessRoleService, - IInstallationService installationService - ) : ControllerBase + ILogger logger, + IAccessRoleService accessRoleService, + IInstallationService installationService + ) : ControllerBase { /// /// List all access roles in the Flotilla database @@ -54,7 +54,9 @@ public async Task>> GetAccessRoles() [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create([FromBody] CreateAccessRoleQuery accessRoleQuery) + public async Task> Create( + [FromBody] CreateAccessRoleQuery accessRoleQuery + ) { logger.LogInformation("Creating new access role"); try @@ -62,7 +64,10 @@ public async Task> Create([FromBody] CreateAccessRoleQu if (accessRoleQuery.AccessLevel == RoleAccessLevel.ADMIN) return Unauthorized("Cannot create admin roles using API endpoints"); - var installation = await installationService.ReadByInstallationCode(accessRoleQuery.InstallationCode, readOnly: true); + var installation = await installationService.ReadByInstallationCode( + accessRoleQuery.InstallationCode, + readOnly: true + ); if (installation is null) { logger.LogInformation("Installation not found when creating new access roles"); @@ -70,13 +75,22 @@ public async Task> Create([FromBody] CreateAccessRoleQu } var existingAccessRole = await accessRoleService.ReadByInstallation(installation!); - if (existingAccessRole != null && existingAccessRole.RoleName == accessRoleQuery.RoleName) + if ( + existingAccessRole != null + && existingAccessRole.RoleName == accessRoleQuery.RoleName + ) { - logger.LogInformation("An access role for the given installation and role name already exists"); + logger.LogInformation( + "An access role for the given installation and role name already exists" + ); return BadRequest($"Access role already exists"); } - var newAccessRole = await accessRoleService.Create(installation, accessRoleQuery.RoleName, accessRoleQuery.AccessLevel); + var newAccessRole = await accessRoleService.Create( + installation, + accessRoleQuery.RoleName, + accessRoleQuery.AccessLevel + ); logger.LogInformation( "Succesfully created new access role for installation '{installationCode}'", installation.InstallationCode diff --git a/backend/api/Controllers/AreaController.cs b/backend/api/Controllers/AreaController.cs index 554a05db5..596a23aeb 100644 --- a/backend/api/Controllers/AreaController.cs +++ b/backend/api/Controllers/AreaController.cs @@ -10,11 +10,11 @@ namespace Api.Controllers [ApiController] [Route("areas")] public class AreaController( - ILogger logger, - IAreaService areaService, - IDefaultLocalizationPoseService defaultLocalizationPoseService, - IMissionDefinitionService missionDefinitionService - ) : ControllerBase + ILogger logger, + IAreaService areaService, + IDefaultLocalizationPoseService defaultLocalizationPoseService, + IMissionDefinitionService missionDefinitionService + ) : ControllerBase { /// /// Add a new area @@ -35,7 +35,11 @@ public async Task> Create([FromBody] CreateAreaQuery logger.LogInformation("Creating new area"); try { - var existingArea = await areaService.ReadByInstallationAndName(area.InstallationCode, area.AreaName, readOnly: true); + var existingArea = await areaService.ReadByInstallationAndName( + area.InstallationCode, + area.AreaName, + readOnly: true + ); if (existingArea != null) { logger.LogWarning("An area for given name and installation already exists"); @@ -48,11 +52,7 @@ public async Task> Create([FromBody] CreateAreaQuery newArea.Id ); var response = new AreaResponse(newArea); - return CreatedAtAction( - nameof(GetAreaById), - new { id = newArea.Id }, - response - ); + return CreatedAtAction(nameof(GetAreaById), new { id = newArea.Id }, response); } catch (Exception e) { @@ -61,7 +61,6 @@ public async Task> Create([FromBody] CreateAreaQuery } } - /// /// Updates default localization pose /// @@ -76,7 +75,10 @@ public async Task> Create([FromBody] CreateAreaQuery [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> UpdateDefaultLocalizationPose([FromRoute] string areaId, [FromBody] Pose newDefaultLocalizationPose) + public async Task> UpdateDefaultLocalizationPose( + [FromRoute] string areaId, + [FromBody] Pose newDefaultLocalizationPose + ) { logger.LogInformation("Updating default localization pose on area '{areaId}'", areaId); try @@ -95,11 +97,12 @@ public async Task> UpdateDefaultLocalizationPose([Fro } else { - area.DefaultLocalizationPose = new DefaultLocalizationPose(newDefaultLocalizationPose); + area.DefaultLocalizationPose = new DefaultLocalizationPose( + newDefaultLocalizationPose + ); area = await areaService.Update(area); } - return Ok(new AreaResponse(area)); } catch (Exception e) @@ -109,7 +112,6 @@ public async Task> UpdateDefaultLocalizationPose([Fro } } - /// /// Deletes the area with the specified id from the database. /// @@ -151,7 +153,9 @@ public async Task> DeleteArea([FromRoute] string id) [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetAreas([FromQuery] AreaQueryStringParameters parameters) + public async Task>> GetAreas( + [FromQuery] AreaQueryStringParameters parameters + ) { PagedList areas; try @@ -214,7 +218,9 @@ public async Task> GetAreaById([FromRoute] string id) [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetAreaByDeckId([FromRoute] string deckId) + public async Task>> GetAreaByDeckId( + [FromRoute] string deckId + ) { try { @@ -243,7 +249,9 @@ public async Task>> GetAreaByDeckId([FromRoute] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetMissionDefinitionsInArea([FromRoute] string id) + public async Task< + ActionResult> + > GetMissionDefinitionsInArea([FromRoute] string id) { try { @@ -251,8 +259,13 @@ public async Task>> GetMissionDefi if (area == null) return NotFound($"Could not find area with id {id}"); - var missionDefinitions = await missionDefinitionService.ReadByInspectionAreaId(area.Deck.Id, readOnly: true); - var missionDefinitionResponses = missionDefinitions.FindAll(m => !m.IsDeprecated).Select(m => new MissionDefinitionResponse(m)); + var missionDefinitions = await missionDefinitionService.ReadByInspectionAreaId( + area.Deck.Id, + readOnly: true + ); + var missionDefinitionResponses = missionDefinitions + .FindAll(m => !m.IsDeprecated) + .Select(m => new MissionDefinitionResponse(m)); return Ok(missionDefinitionResponses); } catch (Exception e) diff --git a/backend/api/Controllers/DeckController.cs b/backend/api/Controllers/DeckController.cs index dad162584..c8bf05145 100644 --- a/backend/api/Controllers/DeckController.cs +++ b/backend/api/Controllers/DeckController.cs @@ -10,14 +10,14 @@ namespace Api.Controllers [ApiController] [Route("decks")] public class DeckController( - ILogger logger, - IMapService mapService, - IDeckService deckService, - IDefaultLocalizationPoseService defaultLocalizationPoseService, - IInstallationService installationService, - IPlantService plantService, - IMissionDefinitionService missionDefinitionService - ) : ControllerBase + ILogger logger, + IMapService mapService, + IDeckService deckService, + IDefaultLocalizationPoseService defaultLocalizationPoseService, + IInstallationService installationService, + IPlantService plantService, + IMissionDefinitionService missionDefinitionService + ) : ControllerBase { /// /// List all decks in the Flotilla database @@ -60,7 +60,9 @@ public async Task>> GetDecks() [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetDecksByInstallationCode([FromRoute] string installationCode) + public async Task>> GetDecksByInstallationCode( + [FromRoute] string installationCode + ) { try { @@ -113,7 +115,9 @@ public async Task> GetDeckById([FromRoute] string id) [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetMissionDefinitionsInDeck([FromRoute] string deckId) + public async Task< + ActionResult> + > GetMissionDefinitionsInDeck([FromRoute] string deckId) { try { @@ -121,8 +125,13 @@ public async Task>> GetMissionDefi if (deck == null) return NotFound($"Could not find deck with id {deckId}"); - var missionDefinitions = await missionDefinitionService.ReadByInspectionAreaId(deck.Id, readOnly: true); - var missionDefinitionResponses = missionDefinitions.FindAll(m => !m.IsDeprecated).Select(m => new MissionDefinitionResponse(m)); + var missionDefinitions = await missionDefinitionService.ReadByInspectionAreaId( + deck.Id, + readOnly: true + ); + var missionDefinitionResponses = missionDefinitions + .FindAll(m => !m.IsDeprecated) + .Select(m => new MissionDefinitionResponse(m)); return Ok(missionDefinitionResponses); } catch (Exception e) @@ -150,17 +159,31 @@ public async Task> Create([FromBody] CreateDeckQuery logger.LogInformation("Creating new deck"); try { - var existingInstallation = await installationService.ReadByInstallationCode(deck.InstallationCode, readOnly: true); + var existingInstallation = await installationService.ReadByInstallationCode( + deck.InstallationCode, + readOnly: true + ); if (existingInstallation == null) { - return NotFound($"Could not find installation with name {deck.InstallationCode}"); + return NotFound( + $"Could not find installation with name {deck.InstallationCode}" + ); } - var existingPlant = await plantService.ReadByInstallationAndPlantCode(existingInstallation, deck.PlantCode, readOnly: true); + var existingPlant = await plantService.ReadByInstallationAndPlantCode( + existingInstallation, + deck.PlantCode, + readOnly: true + ); if (existingPlant == null) { return NotFound($"Could not find plant with name {deck.PlantCode}"); } - var existingDeck = await deckService.ReadByInstallationAndPlantAndName(existingInstallation, existingPlant, deck.Name, readOnly: true); + var existingDeck = await deckService.ReadByInstallationAndPlantAndName( + existingInstallation, + existingPlant, + deck.Name, + readOnly: true + ); if (existingDeck != null) { logger.LogInformation("An deck for given name and deck already exists"); @@ -199,7 +222,10 @@ public async Task> Create([FromBody] CreateDeckQuery [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> UpdateDefaultLocalizationPose([FromRoute] string deckId, [FromBody] CreateDefaultLocalizationPose newDefaultLocalizationPose) + public async Task> UpdateDefaultLocalizationPose( + [FromRoute] string deckId, + [FromBody] CreateDefaultLocalizationPose newDefaultLocalizationPose + ) { logger.LogInformation("Updating default localization pose on deck '{deckId}'", deckId); try @@ -214,12 +240,16 @@ public async Task> UpdateDefaultLocalizationPose([Fro if (deck.DefaultLocalizationPose != null) { deck.DefaultLocalizationPose.Pose = newDefaultLocalizationPose.Pose; - deck.DefaultLocalizationPose.DockingEnabled = newDefaultLocalizationPose.IsDockingStation; + deck.DefaultLocalizationPose.DockingEnabled = + newDefaultLocalizationPose.IsDockingStation; _ = await defaultLocalizationPoseService.Update(deck.DefaultLocalizationPose); } else { - deck.DefaultLocalizationPose = new DefaultLocalizationPose(newDefaultLocalizationPose.Pose, newDefaultLocalizationPose.IsDockingStation); + deck.DefaultLocalizationPose = new DefaultLocalizationPose( + newDefaultLocalizationPose.Pose, + newDefaultLocalizationPose.IsDockingStation + ); deck = await deckService.Update(deck); } @@ -280,23 +310,25 @@ public async Task> GetMapMetadata([FromRoute] string i if (deck.DefaultLocalizationPose is null) { - string errorMessage = $"Deck with id '{deck.Id}' does not have a default localization pose"; + string errorMessage = + $"Deck with id '{deck.Id}' does not have a default localization pose"; logger.LogInformation("{ErrorMessage}", errorMessage); return NotFound(errorMessage); } MapMetadata? mapMetadata; - var positions = new List - { - deck.DefaultLocalizationPose.Pose.Position - }; + var positions = new List { deck.DefaultLocalizationPose.Pose.Position }; try { - mapMetadata = await mapService.ChooseMapFromPositions(positions, deck.Installation.InstallationCode); + mapMetadata = await mapService.ChooseMapFromPositions( + positions, + deck.Installation.InstallationCode + ); } catch (RequestFailedException e) { - string errorMessage = $"An error occurred while retrieving the map for deck {deck.Id}"; + string errorMessage = + $"An error occurred while retrieving the map for deck {deck.Id}"; logger.LogError(e, "{ErrorMessage}", errorMessage); return StatusCode(StatusCodes.Status502BadGateway, errorMessage); } @@ -309,7 +341,9 @@ public async Task> GetMapMetadata([FromRoute] string i if (mapMetadata == null) { - return NotFound("A map which contained at least half of the points in this mission could not be found"); + return NotFound( + "A map which contained at least half of the points in this mission could not be found" + ); } return Ok(mapMetadata); } diff --git a/backend/api/Controllers/EmergencyActionController.cs b/backend/api/Controllers/EmergencyActionController.cs index 493beb81d..60336d622 100644 --- a/backend/api/Controllers/EmergencyActionController.cs +++ b/backend/api/Controllers/EmergencyActionController.cs @@ -3,11 +3,15 @@ using Api.Services.Events; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + namespace Api.Controllers { [ApiController] [Route("emergency-action")] - public class EmergencyActionController(IRobotService robotService, IEmergencyActionService emergencyActionService) : ControllerBase + public class EmergencyActionController( + IRobotService robotService, + IEmergencyActionService emergencyActionService + ) : ControllerBase { /// /// This endpoint will abort the current running mission run and attempt to return the robot to the docking station in the @@ -27,19 +31,25 @@ public class EmergencyActionController(IRobotService robotService, IEmergencyAct [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> AbortCurrentMissionAndSendAllRobotsToDock( - [FromRoute] string installationCode) + [FromRoute] string installationCode + ) { - - var robots = await robotService.ReadRobotsForInstallation(installationCode, readOnly: true); + var robots = await robotService.ReadRobotsForInstallation( + installationCode, + readOnly: true + ); foreach (var robot in robots) { - emergencyActionService.SendRobotToDock(new RobotEmergencyEventArgs(robot.Id, Database.Models.RobotFlotillaStatus.Docked)); - + emergencyActionService.SendRobotToDock( + new RobotEmergencyEventArgs( + robot.Id, + Database.Models.RobotFlotillaStatus.Docked + ) + ); } return NoContent(); - } /// @@ -56,13 +66,22 @@ public async Task> AbortCurrentMissionAndSendAllRobotsToDoc [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> ClearEmergencyStateForAllRobots( - [FromRoute] string installationCode) + [FromRoute] string installationCode + ) { - var robots = await robotService.ReadRobotsForInstallation(installationCode, readOnly: true); + var robots = await robotService.ReadRobotsForInstallation( + installationCode, + readOnly: true + ); foreach (var robot in robots) { - emergencyActionService.ReleaseRobotFromDock(new RobotEmergencyEventArgs(robot.Id, Database.Models.RobotFlotillaStatus.Normal)); + emergencyActionService.ReleaseRobotFromDock( + new RobotEmergencyEventArgs( + robot.Id, + Database.Models.RobotFlotillaStatus.Normal + ) + ); } return NoContent(); diff --git a/backend/api/Controllers/InspectionController.cs b/backend/api/Controllers/InspectionController.cs index 6d2e8974c..4ecd2875f 100644 --- a/backend/api/Controllers/InspectionController.cs +++ b/backend/api/Controllers/InspectionController.cs @@ -1,7 +1,6 @@ using System.Globalization; using Api.Controllers.Models; using Api.Database.Models; - using Api.Services; using Api.Services.MissionLoaders; using Api.Services.Models; @@ -13,10 +12,10 @@ namespace Api.Controllers [ApiController] [Route("inspection")] public class InspectionController( - ILogger logger, - IEchoService echoService, - IInspectionService inspectionService - ) : ControllerBase + ILogger logger, + IEchoService echoService, + IInspectionService inspectionService + ) : ControllerBase { /// /// Updates the Flotilla metadata for an inspection tag @@ -31,15 +30,14 @@ IInspectionService inspectionService [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create([FromRoute] string tagId, [FromBody] IsarZoomDescription zoom) + public async Task> Create( + [FromRoute] string tagId, + [FromBody] IsarZoomDescription zoom + ) { logger.LogInformation($"Updating zoom value for tag with ID {tagId}"); - var newMetadata = new TagInspectionMetadata - { - TagId = tagId, - ZoomDescription = zoom - }; + var newMetadata = new TagInspectionMetadata { TagId = tagId, ZoomDescription = zoom }; try { @@ -68,43 +66,70 @@ public async Task> Create([FromRoute] string [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetInspectionImageById([FromRoute] string installationCode, string taskId) + public async Task> GetInspectionImageById( + [FromRoute] string installationCode, + [FromRoute] string taskId + ) { Inspection? inspection; try { inspection = await inspectionService.ReadByIsarTaskId(taskId, readOnly: true); - if (inspection == null) return NotFound($"Could not find inspection for task with Id {taskId}."); - + if (inspection == null) + return NotFound($"Could not find inspection for task with Id {taskId}."); } catch (Exception e) { - logger.LogError(e, $"Error while finding an inspection with task Id {taskId}"); + logger.LogError(e, "Error while finding an inspection with {taskId}", taskId); return StatusCode(StatusCodes.Status500InternalServerError); } - if (inspection.IsarInspectionId == null) return NotFound($"Could not find isar inspection Id {inspection.IsarInspectionId} for Inspection with task ID {taskId}."); + if (inspection.IsarInspectionId == null) + return NotFound( + $"Could not find isar inspection Id {inspection.IsarInspectionId} for Inspection with task ID {taskId}." + ); - var inspectionData = await inspectionService.GetInspectionStorageInfo(inspection.IsarInspectionId); + var inspectionData = await inspectionService.GetInspectionStorageInfo( + inspection.IsarInspectionId + ); - if (inspectionData == null) return NotFound($"Could not find inspection data for inspection with isar Id {inspection.IsarInspectionId}."); + if (inspectionData == null) + return NotFound( + $"Could not find inspection data for inspection with isar Id {inspection.IsarInspectionId}." + ); - if (!inspectionData.BlobContainer.ToLower(CultureInfo.CurrentCulture).Equals(installationCode.ToLower(CultureInfo.CurrentCulture), StringComparison.Ordinal)) + if ( + !inspectionData + .BlobContainer.ToLower(CultureInfo.CurrentCulture) + .Equals( + installationCode.ToLower(CultureInfo.CurrentCulture), + StringComparison.Ordinal + ) + ) { - return NotFound($"Could not find inspection data for inspection with isar Id {inspection.IsarInspectionId} because blob name {inspectionData.BlobName} does not match installation {installationCode}."); + return NotFound( + $"Could not find inspection data for inspection with isar Id {inspection.IsarInspectionId} because blob name {inspectionData.BlobName} does not match installation {installationCode}." + ); } try { - byte[]? inspectionStream = await inspectionService.FetchInpectionImage(inspectionData.BlobName, inspectionData.BlobContainer, inspectionData.StorageAccount); + byte[]? inspectionStream = await inspectionService.FetchInpectionImage( + inspectionData.BlobName, + inspectionData.BlobContainer, + inspectionData.StorageAccount + ); - if (inspectionStream == null) return NotFound($"Could not retrieve inspection with task Id {taskId}"); + if (inspectionStream == null) + return NotFound($"Could not retrieve inspection with task Id {taskId}"); return File(inspectionStream, "image/png"); } catch (Azure.RequestFailedException) { - return NotFound($"Could not find inspection blob {inspectionData.BlobName} in container {inspectionData.BlobContainer} and storage account {inspectionData.StorageAccount}."); + return NotFound( + $"Could not find inspection blob {inspectionData.BlobName} in container {inspectionData.BlobContainer} and storage account {inspectionData.StorageAccount}." + ); } } } diff --git a/backend/api/Controllers/InspectionFindingController.cs b/backend/api/Controllers/InspectionFindingController.cs index 5885efbf1..4d792f1bf 100644 --- a/backend/api/Controllers/InspectionFindingController.cs +++ b/backend/api/Controllers/InspectionFindingController.cs @@ -9,9 +9,9 @@ namespace Api.Controllers [ApiController] [Route("inspection-findings")] public class InspectionFindingController( - ILogger logger, - IInspectionService inspectionService - ) : ControllerBase + ILogger logger, + IInspectionService inspectionService + ) : ControllerBase { /// /// Associate a new inspection finding with the inspection corresponding to isarTaskId @@ -27,9 +27,15 @@ IInspectionService inspectionService [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> AddFinding([FromBody] InspectionFindingQuery inspectionFinding, [FromRoute] string isarTaskId) + public async Task> AddFinding( + [FromBody] InspectionFindingQuery inspectionFinding, + [FromRoute] string isarTaskId + ) { - logger.LogInformation("Add inspection finding for inspection with isarTaskId '{Id}'", isarTaskId); + logger.LogInformation( + "Add inspection finding for inspection with isarTaskId '{Id}'", + isarTaskId + ); try { var inspection = await inspectionService.AddFinding(inspectionFinding, isarTaskId); @@ -38,11 +44,14 @@ public async Task> AddFinding([FromBody] Inspect { return Ok(inspection.InspectionFindings); } - } catch (Exception e) { - logger.LogError(e, "Error while adding inspection finding to inspection with IsarTaskId '{Id}'", isarTaskId); + logger.LogError( + e, + "Error while adding inspection finding to inspection with IsarTaskId '{Id}'", + isarTaskId + ); return StatusCode(StatusCodes.Status500InternalServerError); } return NotFound($"Could not find any inspection with the provided '{isarTaskId}'"); @@ -72,17 +81,17 @@ public async Task> GetInspections([FromRoute] string id { return Ok(inspection); } - } catch (Exception e) { - logger.LogError(e, "Error while finding an inspection with inspection id '{id}'", id); + logger.LogError( + e, + "Error while finding an inspection with inspection id '{id}'", + id + ); return StatusCode(StatusCodes.Status500InternalServerError); } return NotFound("Could not find any inspection with the provided '{id}'"); } - } - - } diff --git a/backend/api/Controllers/InstallationController.cs b/backend/api/Controllers/InstallationController.cs index e8720e17b..d0fa77fa9 100644 --- a/backend/api/Controllers/InstallationController.cs +++ b/backend/api/Controllers/InstallationController.cs @@ -9,9 +9,9 @@ namespace Api.Controllers [ApiController] [Route("installations")] public class InstallationController( - ILogger logger, - IInstallationService installationService - ) : ControllerBase + ILogger logger, + IInstallationService installationService + ) : ControllerBase { /// /// List all installations in the Flotilla database @@ -65,7 +65,6 @@ public async Task> GetInstallationById([FromRoute] st logger.LogError(e, "Error during GET of installation from database"); throw; } - } /// @@ -81,15 +80,22 @@ public async Task> GetInstallationById([FromRoute] st [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create([FromBody] CreateInstallationQuery installation) + public async Task> Create( + [FromBody] CreateInstallationQuery installation + ) { logger.LogInformation("Creating new installation"); try { - var existingInstallation = await installationService.ReadByInstallationCode(installation.InstallationCode, readOnly: true); + var existingInstallation = await installationService.ReadByInstallationCode( + installation.InstallationCode, + readOnly: true + ); if (existingInstallation != null) { - logger.LogInformation("An installation for given name and installation already exists"); + logger.LogInformation( + "An installation for given name and installation already exists" + ); return BadRequest($"Installation already exists"); } diff --git a/backend/api/Controllers/MapController.cs b/backend/api/Controllers/MapController.cs index fbeb149e2..476559a62 100644 --- a/backend/api/Controllers/MapController.cs +++ b/backend/api/Controllers/MapController.cs @@ -2,6 +2,7 @@ using Api.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + namespace Api.Controllers { [ApiController] @@ -21,11 +22,17 @@ public class MapController(IMapService mapService) : ControllerBase [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task> GetMap([FromRoute] string installationCode, string mapName) + public async Task> GetMap( + [FromRoute] string installationCode, + string mapName + ) { byte[]? mapStream = await mapService.FetchMapImage(mapName, installationCode); - if (mapStream == null) return NotFound($"Could not retrieve map '{mapName}' in installation {installationCode}"); + if (mapStream == null) + return NotFound( + $"Could not retrieve map '{mapName}' in installation {installationCode}" + ); return File(mapStream, "image/png"); } diff --git a/backend/api/Controllers/MediaStreamController.cs b/backend/api/Controllers/MediaStreamController.cs index 090c8af8c..8bfb49118 100644 --- a/backend/api/Controllers/MediaStreamController.cs +++ b/backend/api/Controllers/MediaStreamController.cs @@ -9,10 +9,10 @@ namespace Api.Controllers [ApiController] [Route("media-stream")] public class MediaStreamController( - ILogger logger, - IIsarService isarService, - IRobotService robotService - ) : ControllerBase + ILogger logger, + IIsarService isarService, + IRobotService robotService + ) : ControllerBase { /// /// Request the config for a new media stream connection from ISAR diff --git a/backend/api/Controllers/MissionDefinitionController.cs b/backend/api/Controllers/MissionDefinitionController.cs index 0621def98..ba9b9f861 100644 --- a/backend/api/Controllers/MissionDefinitionController.cs +++ b/backend/api/Controllers/MissionDefinitionController.cs @@ -5,11 +5,16 @@ using Api.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + namespace Api.Controllers { [ApiController] [Route("missions/definitions")] - public class MissionDefinitionController(ILogger logger, IMissionDefinitionService missionDefinitionService, IMissionRunService missionRunService) : ControllerBase + public class MissionDefinitionController( + ILogger logger, + IMissionDefinitionService missionDefinitionService, + IMissionRunService missionRunService + ) : ControllerBase { /// /// List all mission definitions in the Flotilla database @@ -31,7 +36,10 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters PagedList missionDefinitions; try { - missionDefinitions = await missionDefinitionService.ReadAll(parameters, readOnly: true); + missionDefinitions = await missionDefinitionService.ReadAll( + parameters, + readOnly: true + ); } catch (InvalidDataException e) { @@ -46,7 +54,7 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters missionDefinitions.CurrentPage, missionDefinitions.TotalPages, missionDefinitions.HasNext, - missionDefinitions.HasPrevious + missionDefinitions.HasPrevious, }; Response.Headers.Append( @@ -54,7 +62,9 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters JsonSerializer.Serialize(metadata) ); - var missionDefinitionResponses = missionDefinitions.Select(m => new MissionDefinitionResponse(m)); + var missionDefinitionResponses = missionDefinitions.Select( + m => new MissionDefinitionResponse(m) + ); return Ok(missionDefinitionResponses); } @@ -69,14 +79,19 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetMissionDefinitionWithTasksById([FromRoute] string id) + public async Task< + ActionResult + > GetMissionDefinitionWithTasksById([FromRoute] string id) { var missionDefinition = await missionDefinitionService.ReadById(id, readOnly: true); if (missionDefinition == null) { return NotFound($"Could not find mission definition with id {id}"); } - var missionDefinitionResponse = new MissionDefinitionWithTasksResponse(missionDefinitionService, missionDefinition); + var missionDefinitionResponse = new MissionDefinitionWithTasksResponse( + missionDefinitionService, + missionDefinition + ); return Ok(missionDefinitionResponse); } @@ -98,7 +113,10 @@ public async Task> GetNextMissionRun([FromRoute] string { return NotFound($"Could not find mission definition with id {id}"); } - var nextRun = await missionRunService.ReadNextScheduledRunByMissionId(id, readOnly: true); + var nextRun = await missionRunService.ReadNextScheduledRunByMissionId( + id, + readOnly: true + ); return Ok(nextRun); } @@ -163,12 +181,17 @@ [FromBody] UpdateMissionDefinitionQuery missionDefinitionQuery [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> UpdateMissionDefinitionIsDeprecatedById( + public async Task< + ActionResult + > UpdateMissionDefinitionIsDeprecatedById( [FromRoute] string id, [FromBody] UpdateMissionDefinitionIsDeprecatedQuery missionDefinitionIsDeprecatedQuery ) { - logger.LogInformation("Updating mission definition IsDeprected value for id '{Id}'", id); + logger.LogInformation( + "Updating mission definition IsDeprected value for id '{Id}'", + id + ); if (!ModelState.IsValid) { @@ -197,14 +220,19 @@ [FromBody] UpdateMissionDefinitionIsDeprecatedQuery missionDefinitionIsDeprecate [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> DeleteMissionDefinitionWithTasks([FromRoute] string id) + public async Task> DeleteMissionDefinitionWithTasks( + [FromRoute] string id + ) { var missionDefinition = await missionDefinitionService.Delete(id); if (missionDefinition is null) { return NotFound($"Mission definition with id {id} not found"); } - var missionDefinitionResponse = new MissionDefinitionWithTasksResponse(missionDefinitionService, missionDefinition); + var missionDefinitionResponse = new MissionDefinitionWithTasksResponse( + missionDefinitionService, + missionDefinition + ); return Ok(missionDefinitionResponse); } } diff --git a/backend/api/Controllers/MissionLoaderController.cs b/backend/api/Controllers/MissionLoaderController.cs index fcfec5429..da6d1e798 100644 --- a/backend/api/Controllers/MissionLoaderController.cs +++ b/backend/api/Controllers/MissionLoaderController.cs @@ -7,12 +7,17 @@ using Api.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + namespace Api.Controllers { [ApiController] [Route("mission-loader")] [Authorize(Roles = Role.Any)] - public class MissionLoaderController(ILogger logger, IMissionLoader missionLoader, IRobotService robotService) : ControllerBase + public class MissionLoaderController( + ILogger logger, + IMissionLoader missionLoader, + IRobotService robotService + ) : ControllerBase { /// /// List all available missions for the installation @@ -29,7 +34,8 @@ public class MissionLoaderController(ILogger logger, IM [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status502BadGateway)] public async Task>> GetAvailableMissions( - [FromRoute] string? installationCode) + [FromRoute] string? installationCode + ) { IQueryable missionDefinitions; try @@ -52,7 +58,9 @@ public async Task>> GetAvailableMi return new StatusCodeResult(StatusCodes.Status500InternalServerError); } - var missionDefinitionResponses = missionDefinitions.Select(m => new MissionDefinitionResponse(m)).ToList(); + var missionDefinitionResponses = missionDefinitions + .Select(m => new MissionDefinitionResponse(m)) + .ToList(); return Ok(missionDefinitionResponses); } @@ -71,7 +79,9 @@ public async Task>> GetAvailableMi [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task> GetMissionDefinition([FromRoute] string missionSourceId) + public async Task> GetMissionDefinition( + [FromRoute] string missionSourceId + ) { try { @@ -154,7 +164,9 @@ public async Task>> GetActivePlants() if (plants == null) { logger.LogWarning("Could not retrieve robot plants information"); - throw new RobotInformationNotAvailableException("Could not retrieve robot plants information"); + throw new RobotInformationNotAvailableException( + "Could not retrieve robot plants information" + ); } plants = plants.Select(p => p.ToLower(CultureInfo.CurrentCulture)); @@ -163,7 +175,9 @@ public async Task>> GetActivePlants() { var plantInfos = await missionLoader.GetPlantInfos(); - plantInfos = plantInfos.Where(p => plants.Contains(p.PlantCode.ToLower(CultureInfo.CurrentCulture))).ToList(); + plantInfos = plantInfos + .Where(p => plants.Contains(p.PlantCode.ToLower(CultureInfo.CurrentCulture))) + .ToList(); return Ok(plantInfos); } catch (HttpRequestException e) diff --git a/backend/api/Controllers/MissionRunController.cs b/backend/api/Controllers/MissionRunController.cs index 1f3b8c8dc..9c933dc3b 100644 --- a/backend/api/Controllers/MissionRunController.cs +++ b/backend/api/Controllers/MissionRunController.cs @@ -5,11 +5,15 @@ using Api.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + namespace Api.Controllers { [ApiController] [Route("missions/runs")] - public class MissionRunController(ILogger logger, IMissionRunService missionRunService) : ControllerBase + public class MissionRunController( + ILogger logger, + IMissionRunService missionRunService + ) : ControllerBase { /// /// List all mission runs in the Flotilla database @@ -59,7 +63,7 @@ [FromQuery] MissionRunQueryStringParameters parameters missionRuns.CurrentPage, missionRuns.TotalPages, missionRuns.HasNext, - missionRuns.HasPrevious + missionRuns.HasPrevious, }; Response.Headers.Append( @@ -67,9 +71,12 @@ [FromQuery] MissionRunQueryStringParameters parameters JsonSerializer.Serialize(metadata) ); - var missionRunResponses = missionRuns.Select(mission => new MissionRunResponse(mission)); + var missionRunResponses = missionRuns.Select(mission => new MissionRunResponse( + mission + )); return Ok(missionRunResponses); } + /// /// Lookup mission run by specified id. /// @@ -92,6 +99,7 @@ public async Task> GetMissionRunById([FromRoute var missionRunResponse = new MissionRunResponse(missionRun); return Ok(missionRunResponse); } + /// /// Deletes the mission run with the specified id from the database. /// diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index 0db1b9fe9..80998928d 100644 --- a/backend/api/Controllers/MissionSchedulingController.cs +++ b/backend/api/Controllers/MissionSchedulingController.cs @@ -8,25 +8,23 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; - namespace Api.Controllers { [ApiController] [Route("missions")] public class MissionSchedulingController( - IMissionDefinitionService missionDefinitionService, - IMissionRunService missionRunService, - IInstallationService installationService, - IMissionLoader missionLoader, - ILogger logger, - IMapService mapService, - IStidService stidService, - ILocalizationService localizationService, - IRobotService robotService, - ISourceService sourceService, - IDeckService deckService - ) : ControllerBase - + IMissionDefinitionService missionDefinitionService, + IMissionRunService missionRunService, + IInstallationService installationService, + IMissionLoader missionLoader, + ILogger logger, + IMapService mapService, + IStidService stidService, + ILocalizationService localizationService, + IRobotService robotService, + ISourceService sourceService, + IDeckService deckService + ) : ControllerBase { /// /// Rerun a mission run, running only the parts that did not previously complete @@ -48,21 +46,43 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery ) { Robot robot; - try { robot = await robotService.GetRobotWithPreCheck(scheduledMissionQuery.RobotId, readOnly: true); } - catch (Exception e) when (e is RobotNotFoundException) { return NotFound(e.Message); } - catch (Exception e) when (e is RobotPreCheckFailedException) { return BadRequest(e.Message); } + try + { + robot = await robotService.GetRobotWithPreCheck( + scheduledMissionQuery.RobotId, + readOnly: true + ); + } + catch (Exception e) when (e is RobotNotFoundException) + { + return NotFound(e.Message); + } + catch (Exception e) when (e is RobotPreCheckFailedException) + { + return BadRequest(e.Message); + } var missionRun = await missionRunService.ReadById(missionRunId, readOnly: true); - if (missionRun == null) return NotFound("Mission run not found"); - - var missionTasks = missionRun.Tasks.Where((t) => t.Status != Database.Models.TaskStatus.Successful && t.Status != Database.Models.TaskStatus.PartiallySuccessful).Select((t) => new MissionTask(t, Database.Models.TaskStatus.NotStarted)).ToList(); + if (missionRun == null) + return NotFound("Mission run not found"); + + var missionTasks = missionRun + .Tasks.Where( + (t) => + t.Status != Database.Models.TaskStatus.Successful + && t.Status != Database.Models.TaskStatus.PartiallySuccessful + ) + .Select((t) => new MissionTask(t, Database.Models.TaskStatus.NotStarted)) + .ToList(); - if (missionTasks == null || missionTasks.Count == 0) return NotFound("No unfinished mission tasks were found for the requested mission"); + if (missionTasks == null || missionTasks.Count == 0) + return NotFound("No unfinished mission tasks were found for the requested mission"); foreach (var task in missionTasks) { task.Id = Guid.NewGuid().ToString(); - if (task.Inspection != null) task.Inspection.Id = Guid.NewGuid().ToString(); + if (task.Inspection != null) + task.Inspection.Id = Guid.NewGuid().ToString(); } var newMissionRun = new MissionRun @@ -75,7 +95,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery Tasks = missionTasks, DesiredStartTime = scheduledMissionQuery.DesiredStartTime ?? DateTime.UtcNow, InstallationCode = missionRun.InstallationCode, - InspectionArea = missionRun.InspectionArea + InspectionArea = missionRun.InspectionArea, }; if (newMissionRun.Tasks.Any()) @@ -91,13 +111,12 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery } catch (UnsupportedRobotCapabilityException) { - return BadRequest($"The robot {robot.Name} does not have the necessary sensors to run the mission."); + return BadRequest( + $"The robot {robot.Name} does not have the necessary sensors to run the mission." + ); } - return CreatedAtAction(nameof(Rerun), new - { - id = newMissionRun.Id - }, newMissionRun); + return CreatedAtAction(nameof(Rerun), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -120,26 +139,59 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery ) { Robot robot; - try { robot = await robotService.GetRobotWithPreCheck(scheduledMissionQuery.RobotId, readOnly: true); } - catch (Exception e) when (e is RobotNotFoundException) { return NotFound(e.Message); } - catch (Exception e) when (e is RobotPreCheckFailedException) { return BadRequest(e.Message); } + try + { + robot = await robotService.GetRobotWithPreCheck( + scheduledMissionQuery.RobotId, + readOnly: true + ); + } + catch (Exception e) when (e is RobotNotFoundException) + { + return NotFound(e.Message); + } + catch (Exception e) when (e is RobotPreCheckFailedException) + { + return BadRequest(e.Message); + } - var missionDefinition = await missionDefinitionService.ReadById(missionDefinitionId, readOnly: true); + var missionDefinition = await missionDefinitionService.ReadById( + missionDefinitionId, + readOnly: true + ); if (missionDefinition == null) { return NotFound("Mission definition not found"); } else if (missionDefinition.InspectionArea == null) { - logger.LogWarning("Mission definition with ID {id} does not have an inspection area when scheduling", missionDefinition.Id); + logger.LogWarning( + "Mission definition with ID {id} does not have an inspection area when scheduling", + missionDefinition.Id + ); } - try { await localizationService.EnsureRobotIsOnSameInstallationAsMission(robot, missionDefinition); } - catch (InstallationNotFoundException e) { return NotFound(e.Message); } - catch (RobotNotInSameInstallationAsMissionException e) { return Conflict(e.Message); } + try + { + await localizationService.EnsureRobotIsOnSameInstallationAsMission( + robot, + missionDefinition + ); + } + catch (InstallationNotFoundException e) + { + return NotFound(e.Message); + } + catch (RobotNotInSameInstallationAsMissionException e) + { + return Conflict(e.Message); + } - var missionTasks = await missionLoader.GetTasksForMission(missionDefinition.Source.SourceId); - if (missionTasks == null) return NotFound("No mission tasks were found for the requested mission"); + var missionTasks = await missionLoader.GetTasksForMission( + missionDefinition.Source.SourceId + ); + if (missionTasks == null) + return NotFound("No mission tasks were found for the requested mission"); var missionRun = new MissionRun { @@ -151,7 +203,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery DesiredStartTime = scheduledMissionQuery.DesiredStartTime ?? DateTime.UtcNow, Tasks = missionTasks, InstallationCode = missionDefinition.InstallationCode, - InspectionArea = missionDefinition.InspectionArea + InspectionArea = missionDefinition.InspectionArea, }; if (missionRun.Tasks.Any()) @@ -166,13 +218,12 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery } catch (UnsupportedRobotCapabilityException) { - return BadRequest($"The robot {robot.Name} does not have the necessary sensors to run the mission."); + return BadRequest( + $"The robot {robot.Name} does not have the necessary sensors to run the mission." + ); } - return CreatedAtAction(nameof(Schedule), new - { - id = newMissionRun.Id - }, newMissionRun); + return CreatedAtAction(nameof(Schedule), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -195,10 +246,24 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery ) { Robot robot; - try { robot = await robotService.GetRobotWithPreCheck(scheduledMissionQuery.RobotId, readOnly: true); } - catch (Exception e) when (e is RobotNotFoundException) { return NotFound(e.Message); } - catch (Exception e) when (e is RobotPreCheckFailedException) { return BadRequest(e.Message); } - string missionSourceId = scheduledMissionQuery.MissionSourceId.ToString(CultureInfo.CurrentCulture); + try + { + robot = await robotService.GetRobotWithPreCheck( + scheduledMissionQuery.RobotId, + readOnly: true + ); + } + catch (Exception e) when (e is RobotNotFoundException) + { + return NotFound(e.Message); + } + catch (Exception e) when (e is RobotPreCheckFailedException) + { + return BadRequest(e.Message); + } + string missionSourceId = scheduledMissionQuery.MissionSourceId.ToString( + CultureInfo.CurrentCulture + ); MissionDefinition? missionDefinition; try { @@ -212,10 +277,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery { if (e.StatusCode.HasValue && (int)e.StatusCode.Value == 404) { - logger.LogWarning( - "Could not find mission with id={Id}", - missionSourceId - ); + logger.LogWarning("Could not find mission with id={Id}", missionSourceId); return NotFound("Mission not found"); } @@ -236,51 +298,66 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery return StatusCode(StatusCodes.Status502BadGateway, Message); } - var missionTasks = await missionLoader.GetTasksForMission(missionSourceId); List missionAreas; missionAreas = missionTasks .Where(t => t.TagId != null) - .Select(t => stidService.GetTagArea(t.TagId!, scheduledMissionQuery.InstallationCode).Result) + .Select(t => + stidService.GetTagArea(t.TagId!, scheduledMissionQuery.InstallationCode).Result + ) .ToList(); - var missionDeckNames = missionAreas.Where(a => a != null).Select(a => a!.Deck.Name).Distinct().ToList(); + var missionDeckNames = missionAreas + .Where(a => a != null) + .Select(a => a!.Deck.Name) + .Distinct() + .ToList(); if (missionDeckNames.Count > 1) { string joinedMissionDeckNames = string.Join(", ", [.. missionDeckNames]); - logger.LogWarning($"Mission {missionDefinition.Name} has tags on more than one deck. The decks are: {joinedMissionDeckNames}."); + logger.LogWarning( + $"Mission {missionDefinition.Name} has tags on more than one deck. The decks are: {joinedMissionDeckNames}." + ); } - var sortedAreas = missionAreas.GroupBy(i => i).OrderByDescending(grp => grp.Count()).Select(grp => grp.Key); + var sortedAreas = missionAreas + .GroupBy(i => i) + .OrderByDescending(grp => grp.Count()) + .Select(grp => grp.Key); var area = sortedAreas.First(); if (area == null && sortedAreas.Count() > 1) { - logger.LogWarning($"Most common area in mission {missionDefinition.Name} is null. Will use second most common area."); + logger.LogWarning( + $"Most common area in mission {missionDefinition.Name} is null. Will use second most common area." + ); area = sortedAreas.Skip(1).First(); - } if (area == null) { - logger.LogError($"Mission {missionDefinition.Name} doesn't have any tags with valid area."); + logger.LogError( + $"Mission {missionDefinition.Name} doesn't have any tags with valid area." + ); return NotFound($"No area found for mission '{missionDefinition.Name}'."); } - var source = await sourceService.CheckForExistingSource(scheduledMissionQuery.MissionSourceId); + var source = await sourceService.CheckForExistingSource( + scheduledMissionQuery.MissionSourceId + ); MissionDefinition? existingMissionDefinition = null; if (source == null) { source = await sourceService.Create( - new Source - { - SourceId = $"{missionDefinition.Id}", - } + new Source { SourceId = $"{missionDefinition.Id}" } ); } else { - var missionDefinitions = await missionDefinitionService.ReadBySourceId(source.SourceId, readOnly: true); + var missionDefinitions = await missionDefinitionService.ReadBySourceId( + source.SourceId, + readOnly: true + ); if (missionDefinitions.Count > 0) { existingMissionDefinition = missionDefinitions.First(); @@ -292,20 +369,25 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery } } - var scheduledMissionDefinition = existingMissionDefinition ?? new MissionDefinition - { - Id = Guid.NewGuid().ToString(), - Source = source, - Name = missionDefinition.Name, - InspectionFrequency = scheduledMissionQuery.InspectionFrequency, - InstallationCode = scheduledMissionQuery.InstallationCode, - InspectionArea = area.Deck, - Map = new MapMetadata() - }; + var scheduledMissionDefinition = + existingMissionDefinition + ?? new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Source = source, + Name = missionDefinition.Name, + InspectionFrequency = scheduledMissionQuery.InspectionFrequency, + InstallationCode = scheduledMissionQuery.InstallationCode, + InspectionArea = area.Deck, + Map = new MapMetadata(), + }; if (scheduledMissionDefinition.InspectionArea == null) { - logger.LogWarning("Mission definition with ID {id} does not have an inspection area when scheduling", scheduledMissionDefinition.Id); + logger.LogWarning( + "Mission definition with ID {id} does not have an inspection area when scheduling", + scheduledMissionDefinition.Id + ); } var missionRun = new MissionRun @@ -318,10 +400,12 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery DesiredStartTime = scheduledMissionQuery.DesiredStartTime ?? DateTime.UtcNow, Tasks = missionTasks, InstallationCode = scheduledMissionQuery.InstallationCode, - InspectionArea = scheduledMissionDefinition.InspectionArea + InspectionArea = scheduledMissionDefinition.InspectionArea, }; - scheduledMissionDefinition.Map = await mapService.ChooseMapFromMissionRunTasks(missionRun); + scheduledMissionDefinition.Map = await mapService.ChooseMapFromMissionRunTasks( + missionRun + ); if (missionRun.Tasks.Any()) { @@ -333,9 +417,17 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery await missionDefinitionService.Create(scheduledMissionDefinition); } - if (missionRun.Robot.CurrentInspectionArea != null && !await localizationService.RobotIsOnSameDeckAsMission(missionRun.Robot.Id, missionRun.InspectionArea!.Id)) + if ( + missionRun.Robot.CurrentInspectionArea != null + && !await localizationService.RobotIsOnSameDeckAsMission( + missionRun.Robot.Id, + missionRun.InspectionArea!.Id + ) + ) { - return Conflict($"The robot {missionRun.Robot.Name} is assumed to be in a different inspection area so the mission was not scheduled."); + return Conflict( + $"The robot {missionRun.Robot.Name} is assumed to be in a different inspection area so the mission was not scheduled." + ); } MissionRun newMissionRun; @@ -345,13 +437,12 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery } catch (UnsupportedRobotCapabilityException) { - return BadRequest($"The robot {robot.Name} does not have the necessary sensors to run the mission."); + return BadRequest( + $"The robot {robot.Name} does not have the necessary sensors to run the mission." + ); } - return CreatedAtAction(nameof(Create), new - { - id = newMissionRun.Id - }, newMissionRun); + return CreatedAtAction(nameof(Create), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -375,23 +466,54 @@ [FromBody] CustomMissionQuery customMissionQuery ) { Robot robot; - try { robot = await robotService.GetRobotWithPreCheck(customMissionQuery.RobotId, readOnly: true); } - catch (Exception e) when (e is RobotNotFoundException) { return NotFound(e.Message); } - catch (Exception e) when (e is RobotPreCheckFailedException) { return BadRequest(e.Message); } + try + { + robot = await robotService.GetRobotWithPreCheck( + customMissionQuery.RobotId, + readOnly: true + ); + } + catch (Exception e) when (e is RobotNotFoundException) + { + return NotFound(e.Message); + } + catch (Exception e) when (e is RobotPreCheckFailedException) + { + return BadRequest(e.Message); + } - var installation = await installationService.ReadByInstallationCode(customMissionQuery.InstallationCode, readOnly: true); - if (installation == null) { return NotFound($"Could not find installation with name {customMissionQuery.InstallationCode}"); } + var installation = await installationService.ReadByInstallationCode( + customMissionQuery.InstallationCode, + readOnly: true + ); + if (installation == null) + { + return NotFound( + $"Could not find installation with name {customMissionQuery.InstallationCode}" + ); + } - var missionTasks = customMissionQuery.Tasks.Select(task => new MissionTask(task)).ToList(); + var missionTasks = customMissionQuery + .Tasks.Select(task => new MissionTask(task)) + .ToList(); MissionDefinition? customMissionDefinition; Deck? inspectionArea = null; try { - if (customMissionQuery.InspectionAreaName != null) { inspectionArea = await deckService.ReadByInstallationAndName(customMissionQuery.InstallationCode, customMissionQuery.InspectionAreaName, readOnly: true); } + if (customMissionQuery.InspectionAreaName != null) + { + inspectionArea = await deckService.ReadByInstallationAndName( + customMissionQuery.InstallationCode, + customMissionQuery.InspectionAreaName, + readOnly: true + ); + } if (inspectionArea == null) { - throw new AreaNotFoundException($"No inspection area with name {customMissionQuery.InspectionAreaName} in installation {customMissionQuery.InstallationCode} was found"); + throw new AreaNotFoundException( + $"No inspection area with name {customMissionQuery.InspectionAreaName} in installation {customMissionQuery.InstallationCode} was found" + ); } var source = await sourceService.CheckForExistingSourceFromTasks(missionTasks); @@ -403,33 +525,64 @@ [FromBody] CustomMissionQuery customMissionQuery } else { - var missionDefinitions = await missionDefinitionService.ReadBySourceId(source.SourceId, readOnly: true); - if (missionDefinitions.Count > 0) { existingMissionDefinition = missionDefinitions.First(); } + var missionDefinitions = await missionDefinitionService.ReadBySourceId( + source.SourceId, + readOnly: true + ); + if (missionDefinitions.Count > 0) + { + existingMissionDefinition = missionDefinitions.First(); + } } - customMissionDefinition = existingMissionDefinition ?? new MissionDefinition - { - Id = Guid.NewGuid().ToString(), - Source = source, - Name = customMissionQuery.Name, - InspectionFrequency = customMissionQuery.InspectionFrequency, - InstallationCode = customMissionQuery.InstallationCode, - InspectionArea = inspectionArea, - Map = new MapMetadata() - }; + customMissionDefinition = + existingMissionDefinition + ?? new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Source = source, + Name = customMissionQuery.Name, + InspectionFrequency = customMissionQuery.InspectionFrequency, + InstallationCode = customMissionQuery.InstallationCode, + InspectionArea = inspectionArea, + Map = new MapMetadata(), + }; if (existingMissionDefinition == null) { - customMissionDefinition.Map = await mapService.ChooseMapFromPositions(missionTasks.Select(t => t.RobotPose.Position).ToList(), customMissionQuery.InstallationCode); + customMissionDefinition.Map = await mapService.ChooseMapFromPositions( + missionTasks.Select(t => t.RobotPose.Position).ToList(), + customMissionQuery.InstallationCode + ); await missionDefinitionService.Create(customMissionDefinition); } } - catch (SourceException e) { return StatusCode(StatusCodes.Status502BadGateway, e.Message); } - catch (AreaNotFoundException) { return NotFound($"No area with name {customMissionQuery.InspectionAreaName} in installation {customMissionQuery.InstallationCode} was found"); } + catch (SourceException e) + { + return StatusCode(StatusCodes.Status502BadGateway, e.Message); + } + catch (AreaNotFoundException) + { + return NotFound( + $"No area with name {customMissionQuery.InspectionAreaName} in installation {customMissionQuery.InstallationCode} was found" + ); + } - try { await localizationService.EnsureRobotIsOnSameInstallationAsMission(robot, customMissionDefinition); } - catch (InstallationNotFoundException e) { return NotFound(e.Message); } - catch (RobotNotInSameInstallationAsMissionException e) { return Conflict(e.Message); } + try + { + await localizationService.EnsureRobotIsOnSameInstallationAsMission( + robot, + customMissionDefinition + ); + } + catch (InstallationNotFoundException e) + { + return NotFound(e.Message); + } + catch (RobotNotInSameInstallationAsMissionException e) + { + return Conflict(e.Message); + } MissionRun? newMissionRun; try @@ -446,27 +599,49 @@ [FromBody] CustomMissionQuery customMissionQuery DesiredStartTime = customMissionQuery.DesiredStartTime ?? DateTime.UtcNow, Tasks = missionTasks, InstallationCode = customMissionQuery.InstallationCode, - InspectionArea = inspectionArea + InspectionArea = inspectionArea, }; - if (scheduledMission.Tasks.Any()) { scheduledMission.CalculateEstimatedDuration(); } + if (scheduledMission.Tasks.Any()) + { + scheduledMission.CalculateEstimatedDuration(); + } - if (scheduledMission.Robot.CurrentInspectionArea != null && !await localizationService.RobotIsOnSameDeckAsMission(scheduledMission.Robot.Id, scheduledMission.InspectionArea.Id)) + if ( + scheduledMission.Robot.CurrentInspectionArea != null + && !await localizationService.RobotIsOnSameDeckAsMission( + scheduledMission.Robot.Id, + scheduledMission.InspectionArea.Id + ) + ) { - return Conflict($"The robot {scheduledMission.Robot.Name} is assumed to be in a different inspection area so the mission was not scheduled."); + return Conflict( + $"The robot {scheduledMission.Robot.Name} is assumed to be in a different inspection area so the mission was not scheduled." + ); } newMissionRun = await missionRunService.Create(scheduledMission); } - catch (Exception e) when (e is UnsupportedRobotCapabilityException) { return BadRequest(e.Message); } - catch (Exception e) when (e is MissionNotFoundException) { return NotFound(e.Message); } - catch (Exception e) when (e is RobotNotFoundException) { return NotFound(e.Message); } - catch (Exception e) when (e is UnsupportedRobotCapabilityException) { return BadRequest($"The robot {robot.Name} does not have the necessary sensors to run the mission."); } - - return CreatedAtAction(nameof(Create), new + catch (Exception e) when (e is UnsupportedRobotCapabilityException) + { + return BadRequest(e.Message); + } + catch (Exception e) when (e is MissionNotFoundException) { - id = newMissionRun.Id - }, newMissionRun); + return NotFound(e.Message); + } + catch (Exception e) when (e is RobotNotFoundException) + { + return NotFound(e.Message); + } + catch (Exception e) when (e is UnsupportedRobotCapabilityException) + { + return BadRequest( + $"The robot {robot.Name} does not have the necessary sensors to run the mission." + ); + } + + return CreatedAtAction(nameof(Create), new { id = newMissionRun.Id }, newMissionRun); } } } diff --git a/backend/api/Controllers/Models/AreaResponse.cs b/backend/api/Controllers/Models/AreaResponse.cs index b078f9f44..9437a008f 100644 --- a/backend/api/Controllers/Models/AreaResponse.cs +++ b/backend/api/Controllers/Models/AreaResponse.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Api.Database.Models; + namespace Api.Controllers.Models { public class AreaResponse @@ -23,6 +24,7 @@ public class AreaResponse [JsonConstructor] #nullable disable public AreaResponse() { } + #nullable enable public AreaResponse(Area area) diff --git a/backend/api/Controllers/Models/CustomMissionQuery.cs b/backend/api/Controllers/Models/CustomMissionQuery.cs index 5b896914c..a584e4d23 100644 --- a/backend/api/Controllers/Models/CustomMissionQuery.cs +++ b/backend/api/Controllers/Models/CustomMissionQuery.cs @@ -1,5 +1,6 @@ using Api.Database.Models; using Api.Services.Models; + namespace Api.Controllers.Models { public struct CustomInspectionQuery diff --git a/backend/api/Controllers/Models/DeckResponse.cs b/backend/api/Controllers/Models/DeckResponse.cs index 364c26d39..8a6a9beb1 100644 --- a/backend/api/Controllers/Models/DeckResponse.cs +++ b/backend/api/Controllers/Models/DeckResponse.cs @@ -18,6 +18,7 @@ public class DeckResponse [JsonConstructor] #nullable disable public DeckResponse() { } + #nullable enable public DeckResponse(Deck deck) diff --git a/backend/api/Controllers/Models/InspectionFindingQuery.cs b/backend/api/Controllers/Models/InspectionFindingQuery.cs index 6015eee7b..a42de5cc5 100644 --- a/backend/api/Controllers/Models/InspectionFindingQuery.cs +++ b/backend/api/Controllers/Models/InspectionFindingQuery.cs @@ -2,11 +2,8 @@ { public struct InspectionFindingQuery { - public DateTime InspectionDate { get; set; } public string Finding { get; set; } } - } - diff --git a/backend/api/Controllers/Models/MissionDefinitionResponse.cs b/backend/api/Controllers/Models/MissionDefinitionResponse.cs index 5b732443d..d666e205f 100644 --- a/backend/api/Controllers/Models/MissionDefinitionResponse.cs +++ b/backend/api/Controllers/Models/MissionDefinitionResponse.cs @@ -47,7 +47,10 @@ public MissionDefinitionResponse(MissionDefinition missionDefinition) InstallationCode = missionDefinition.InstallationCode; Comment = missionDefinition.Comment; InspectionFrequency = missionDefinition.InspectionFrequency; - InspectionArea = missionDefinition.InspectionArea != null ? new DeckResponse(missionDefinition.InspectionArea) : null; + InspectionArea = + missionDefinition.InspectionArea != null + ? new DeckResponse(missionDefinition.InspectionArea) + : null; LastSuccessfulRun = missionDefinition.LastSuccessfulRun; IsDeprecated = missionDefinition.IsDeprecated; SourceId = missionDefinition.Source.SourceId; @@ -55,13 +58,17 @@ public MissionDefinitionResponse(MissionDefinition missionDefinition) } } - public class MissionDefinitionWithTasksResponse(IMissionDefinitionService service, MissionDefinition missionDefinition) + public class MissionDefinitionWithTasksResponse( + IMissionDefinitionService service, + MissionDefinition missionDefinition + ) { [JsonPropertyName("id")] public string Id { get; } = missionDefinition.Id; [JsonPropertyName("tasks")] - public List Tasks { get; } = service.GetTasksFromSource(missionDefinition.Source).Result!; + public List Tasks { get; } = + service.GetTasksFromSource(missionDefinition.Source).Result!; [JsonPropertyName("name")] public string Name { get; } = missionDefinition.Name; diff --git a/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs index 402e98a03..507b1852e 100644 --- a/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs +++ b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs @@ -103,6 +103,5 @@ public MissionRunQueryStringParameters() public long MaxDesiredStartTime { get; set; } = DateTimeOffset.MaxValue.ToUnixTimeSeconds(); #endregion Time Filters - } } diff --git a/backend/api/Controllers/Models/MissionRunResponse.cs b/backend/api/Controllers/Models/MissionRunResponse.cs index 107ee7371..ae11e5946 100644 --- a/backend/api/Controllers/Models/MissionRunResponse.cs +++ b/backend/api/Controllers/Models/MissionRunResponse.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Api.Database.Models; + namespace Api.Controllers.Models { public class MissionRunResponse @@ -43,6 +44,7 @@ public class MissionRunResponse [JsonConstructor] #nullable disable public MissionRunResponse() { } + #nullable enable public MissionRunResponse(MissionRun mission) @@ -55,7 +57,8 @@ public MissionRunResponse(MissionRun mission) StatusReason = mission.StatusReason; Comment = mission.Comment; InstallationCode = mission.InstallationCode; - InspectionArea = mission.InspectionArea != null ? new DeckResponse(mission.InspectionArea) : null; + InspectionArea = + mission.InspectionArea != null ? new DeckResponse(mission.InspectionArea) : null; Robot = new RobotResponse(mission.Robot); Status = mission.Status; IsCompleted = mission.IsCompleted; @@ -66,6 +69,5 @@ public MissionRunResponse(MissionRun mission) Tasks = mission.Tasks; MissionRunType = mission.MissionRunType; } - } } diff --git a/backend/api/Controllers/Models/RobotResponse.cs b/backend/api/Controllers/Models/RobotResponse.cs index b34358775..7911135a3 100644 --- a/backend/api/Controllers/Models/RobotResponse.cs +++ b/backend/api/Controllers/Models/RobotResponse.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Api.Database.Models; + namespace Api.Controllers.Models { public class RobotResponse @@ -49,6 +50,7 @@ public class RobotResponse [JsonConstructor] #nullable disable public RobotResponse() { } + #nullable enable public RobotResponse(Robot robot) @@ -59,7 +61,10 @@ public RobotResponse(Robot robot) Model = robot.Model; SerialNumber = robot.SerialNumber; CurrentInstallation = robot.CurrentInstallation; - CurrentInspectionArea = robot.CurrentInspectionArea != null ? new DeckResponse(robot.CurrentInspectionArea) : null; + CurrentInspectionArea = + robot.CurrentInspectionArea != null + ? new DeckResponse(robot.CurrentInspectionArea) + : null; BatteryLevel = robot.BatteryLevel; BatteryState = robot.BatteryState; PressureLevel = robot.PressureLevel; diff --git a/backend/api/Controllers/PlantController.cs b/backend/api/Controllers/PlantController.cs index ef9fe25da..6fdb432ca 100644 --- a/backend/api/Controllers/PlantController.cs +++ b/backend/api/Controllers/PlantController.cs @@ -9,10 +9,10 @@ namespace Api.Controllers [ApiController] [Route("plants")] public class PlantController( - ILogger logger, - IPlantService plantService, - IInstallationService installationService - ) : ControllerBase + ILogger logger, + IPlantService plantService, + IInstallationService installationService + ) : ControllerBase { /// /// List all plants in the Flotilla database @@ -66,7 +66,6 @@ public async Task> GetPlantById([FromRoute] string id) logger.LogError(e, "Error during GET of plant from database"); throw; } - } /// @@ -87,12 +86,21 @@ public async Task> Create([FromBody] CreatePlantQuery plant) logger.LogInformation("Creating new plant"); try { - var existingInstallation = await installationService.ReadByInstallationCode(plant.InstallationCode, readOnly: true); + var existingInstallation = await installationService.ReadByInstallationCode( + plant.InstallationCode, + readOnly: true + ); if (existingInstallation == null) { - return NotFound($"Installation with installation code {plant.InstallationCode} not found"); + return NotFound( + $"Installation with installation code {plant.InstallationCode} not found" + ); } - var existingPlant = await plantService.ReadByInstallationAndPlantCode(existingInstallation, plant.PlantCode, readOnly: true); + var existingPlant = await plantService.ReadByInstallationAndPlantCode( + existingInstallation, + plant.PlantCode, + readOnly: true + ); if (existingPlant != null) { logger.LogInformation("A plant for given name and plant already exists"); @@ -104,11 +112,7 @@ public async Task> Create([FromBody] CreatePlantQuery plant) "Succesfully created new plant with id '{plantId}'", newPlant.Id ); - return CreatedAtAction( - nameof(GetPlantById), - new { id = newPlant.Id }, - newPlant - ); + return CreatedAtAction(nameof(GetPlantById), new { id = newPlant.Id }, newPlant); } catch (Exception e) { diff --git a/backend/api/Controllers/ReturnToHomeController.cs b/backend/api/Controllers/ReturnToHomeController.cs index e26b07500..5a15deb25 100644 --- a/backend/api/Controllers/ReturnToHomeController.cs +++ b/backend/api/Controllers/ReturnToHomeController.cs @@ -3,15 +3,16 @@ using Api.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + namespace Api.Controllers { [ApiController] [Route("return-to-home")] public class ReturnToHomeController( - ILogger logger, - IReturnToHomeService returnToHomeService, - IRobotService robotService - ) : ControllerBase + ILogger logger, + IReturnToHomeService returnToHomeService, + IRobotService robotService + ) : ControllerBase { /// /// Sends the robots to their home. @@ -34,7 +35,10 @@ [FromRoute] string robotId return NotFound(); } - var returnToHomeMission = await returnToHomeService.ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome(robot.Id); + var returnToHomeMission = + await returnToHomeService.ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome( + robot.Id + ); if (returnToHomeMission is null) { string errorMessage = "Error while scheduling Return to Home mission"; diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index c537862ea..a243a3c8c 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -6,19 +6,20 @@ using Api.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; + namespace Api.Controllers { [ApiController] [Route("robots")] public class RobotController( - ILogger logger, - IRobotService robotService, - IIsarService isarService, - IMissionSchedulingService missionSchedulingService, - IRobotModelService robotModelService, - IDeckService deckService, - IErrorHandlingService errorHandlingService - ) : ControllerBase + ILogger logger, + IRobotService robotService, + IIsarService isarService, + IMissionSchedulingService missionSchedulingService, + IRobotModelService robotModelService, + IDeckService deckService, + IErrorHandlingService errorHandlingService + ) : ControllerBase { /// /// List all robots on the installation. @@ -97,12 +98,17 @@ public async Task> GetRobotById([FromRoute] string i [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> CreateRobot([FromBody] CreateRobotQuery robotQuery) + public async Task> CreateRobot( + [FromBody] CreateRobotQuery robotQuery + ) { logger.LogInformation("Creating new robot"); try { - var robotModel = await robotModelService.ReadByRobotType(robotQuery.RobotType, readOnly: true); + var robotModel = await robotModelService.ReadByRobotType( + robotQuery.RobotType, + readOnly: true + ); if (robotModel == null) { return BadRequest( @@ -114,10 +120,11 @@ public async Task> CreateRobot([FromBody] CreateRobo var robotResponses = new RobotResponse(newRobot); logger.LogInformation("Succesfully created new robot"); - return CreatedAtAction(nameof(GetRobotById), new - { - id = newRobot.Id - }, robotResponses); + return CreatedAtAction( + nameof(GetRobotById), + new { id = newRobot.Id }, + robotResponses + ); } catch (Exception e) { @@ -225,14 +232,21 @@ [FromBody] UpdateRobotQuery query } else { - var inspectionArea = await deckService.ReadById(query.InspectionAreaId, readOnly: true); - if (inspectionArea == null) return NotFound($"No inspection area with ID {query.InspectionAreaId} was found"); + var inspectionArea = await deckService.ReadById( + query.InspectionAreaId, + readOnly: true + ); + if (inspectionArea == null) + return NotFound( + $"No inspection area with ID {query.InspectionAreaId} was found" + ); await robotService.UpdateCurrentInspectionArea(id, inspectionArea.Id); robot.CurrentInspectionArea = inspectionArea; } break; case "pose": - if (query.Pose == null) return BadRequest("Cannot set robot pose to null"); + if (query.Pose == null) + return BadRequest("Cannot set robot pose to null"); await robotService.UpdateRobotPose(id, query.Pose); robot.Pose = query.Pose; break; @@ -277,7 +291,11 @@ public async Task> UpdateRobotDeprecated( [FromRoute] bool deprecated ) { - logger.LogInformation("Updating deprecated on robot with id={Id} to deprecated={Deprecated}", id, deprecated); + logger.LogInformation( + "Updating deprecated on robot with id={Id} to deprecated={Deprecated}", + id, + deprecated + ); try { @@ -350,7 +368,8 @@ [FromBody] RobotStatus robotStatus { logger.LogInformation("Updating robot status with id={Id}", id); - if (!ModelState.IsValid) return BadRequest("Invalid data"); + if (!ModelState.IsValid) + return BadRequest("Invalid data"); var robot = await robotService.ReadById(id, readOnly: true); if (robot == null) @@ -368,7 +387,10 @@ [FromBody] RobotStatus robotStatus var robotResponse = new RobotResponse(robot); - if (robotStatus == RobotStatus.Available) missionSchedulingService.TriggerRobotAvailable(new RobotAvailableEventArgs(robot.Id)); + if (robotStatus == RobotStatus.Available) + missionSchedulingService.TriggerRobotAvailable( + new RobotAvailableEventArgs(robot.Id) + ); return Ok(robotResponse); } @@ -403,7 +425,10 @@ public async Task StopMission([FromRoute] string robotId) return NotFound(); } - try { await isarService.StopMission(robot); } + try + { + await isarService.StopMission(robot); + } catch (HttpRequestException e) { const string Message = "Error connecting to ISAR while stopping mission"; @@ -425,12 +450,23 @@ public async Task StopMission([FromRoute] string robotId) catch (MissionNotFoundException) { logger.LogWarning($"No mission was runnning for robot {robot.Id}"); - try { await robotService.UpdateCurrentMissionId(robotId, null); } - catch (RobotNotFoundException e) { return NotFound(e.Message); } - + try + { + await robotService.UpdateCurrentMissionId(robotId, null); + } + catch (RobotNotFoundException e) + { + return NotFound(e.Message); + } + } + try + { + await robotService.UpdateCurrentMissionId(robotId, null); + } + catch (RobotNotFoundException e) + { + return NotFound(e.Message); } - try { await robotService.UpdateCurrentMissionId(robotId, null); } - catch (RobotNotFoundException e) { return NotFound(e.Message); } return NoContent(); } @@ -535,7 +571,6 @@ public async Task ResumeMission([FromRoute] string robotId) return NoContent(); } - /// /// Post new arm position ("battery_change", "transport", "lookout") for the robot with id 'robotId' /// @@ -566,19 +601,24 @@ [FromRoute] string armPosition if (robot.Status is not RobotStatus.Available) { - string errorMessage = $"Robot {robotId} has status ({robot.Status}) and is not available"; + string errorMessage = + $"Robot {robotId} has status ({robot.Status}) and is not available"; logger.LogWarning("{Message}", errorMessage); return Conflict(errorMessage); } if (robot.Deprecated) { - string errorMessage = $"Robot {robotId} is deprecated ({robot.Status}) and cannot run missions"; + string errorMessage = + $"Robot {robotId} is deprecated ({robot.Status}) and cannot run missions"; logger.LogWarning("{Message}", errorMessage); return Conflict(errorMessage); } - try { await isarService.StartMoveArm(robot, armPosition); } + try + { + await isarService.StartMoveArm(robot, armPosition); + } catch (HttpRequestException e) { string errorMessage = $"Error connecting to ISAR at {robot.IsarUri}"; @@ -588,7 +628,8 @@ [FromRoute] string armPosition } catch (MissionException e) { - const string ErrorMessage = "An error occurred while setting the arm position mission"; + const string ErrorMessage = + "An error occurred while setting the arm position mission"; logger.LogError(e, "{Message}", ErrorMessage); return StatusCode(StatusCodes.Status502BadGateway, ErrorMessage); } @@ -617,9 +658,7 @@ [FromRoute] string armPosition [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task ResetRobot( - [FromRoute] string robotId - ) + public async Task ResetRobot([FromRoute] string robotId) { var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) @@ -629,24 +668,36 @@ [FromRoute] string robotId return NotFound(errorMessage); } - try { await missionSchedulingService.AbortAllScheduledMissions(robot.Id, "Aborted: Robot was reset"); } + try + { + await missionSchedulingService.AbortAllScheduledMissions( + robot.Id, + "Aborted: Robot was reset" + ); + } catch (RobotNotFoundException) { - string errorMessage = $"Failed to abort scheduled missions for robot with id {robotId}"; + string errorMessage = + $"Failed to abort scheduled missions for robot with id {robotId}"; logger.LogWarning("{Message}", errorMessage); return NotFound(errorMessage); } - try { await missionSchedulingService.StopCurrentMissionRun(robot.Id); } + try + { + await missionSchedulingService.StopCurrentMissionRun(robot.Id); + } catch (RobotNotFoundException) { - string errorMessage = $"Failed to stop current mission for robot with id {robotId} because the robot was not found"; + string errorMessage = + $"Failed to stop current mission for robot with id {robotId} because the robot was not found"; logger.LogWarning("{Message}", errorMessage); return NotFound(errorMessage); } catch (MissionRunNotFoundException) { - string errorMessage = $"Failed to stop current mission for robot with id {robotId} because the mission was not found"; + string errorMessage = + $"Failed to stop current mission for robot with id {robotId} because the mission was not found"; logger.LogWarning("{Message}", errorMessage); return Conflict(errorMessage); } diff --git a/backend/api/Controllers/RobotModelController.cs b/backend/api/Controllers/RobotModelController.cs index c55f4872f..86a519120 100644 --- a/backend/api/Controllers/RobotModelController.cs +++ b/backend/api/Controllers/RobotModelController.cs @@ -9,9 +9,9 @@ namespace Api.Controllers; [ApiController] [Route("robot-models")] public class RobotModelController( - ILogger logger, - IRobotModelService robotModelService - ) : ControllerBase + ILogger logger, + IRobotModelService robotModelService +) : ControllerBase { /// /// List all robot models in the Flotilla database diff --git a/backend/api/Controllers/SourceController.cs b/backend/api/Controllers/SourceController.cs index 323da4a86..52b20580e 100644 --- a/backend/api/Controllers/SourceController.cs +++ b/backend/api/Controllers/SourceController.cs @@ -8,10 +8,8 @@ namespace Api.Controllers; [ApiController] [Route("sources")] -public class SourceController( - ISourceService sourceService, - ILogger logger - ) : ControllerBase +public class SourceController(ISourceService sourceService, ILogger logger) + : ControllerBase { /// /// List all sources in the Flotilla database diff --git a/backend/api/Controllers/TimeseriesController.cs b/backend/api/Controllers/TimeseriesController.cs index 5ff0822c1..c7ec9bb7d 100644 --- a/backend/api/Controllers/TimeseriesController.cs +++ b/backend/api/Controllers/TimeseriesController.cs @@ -10,9 +10,9 @@ namespace Api.Controllers [Route("timeseries")] [Authorize(Roles = Role.Any)] public class TimeseriesController( - ILogger logger, - ITimeseriesService timeseriesService - ) : ControllerBase + ILogger logger, + ITimeseriesService timeseriesService + ) : ControllerBase { /// /// Get pressure timeseries @@ -35,9 +35,7 @@ [FromQuery] TimeseriesQueryStringParameters queryStringParameters try { var robotPressureTimeseries = - await timeseriesService.ReadAll( - queryStringParameters - ); + await timeseriesService.ReadAll(queryStringParameters); return Ok(robotPressureTimeseries); } catch (Exception e) diff --git a/backend/api/Database/Context/DesignTimeContextFactory.cs b/backend/api/Database/Context/DesignTimeContextFactory.cs index d6a9ed69a..ecff89f0c 100644 --- a/backend/api/Database/Context/DesignTimeContextFactory.cs +++ b/backend/api/Database/Context/DesignTimeContextFactory.cs @@ -30,7 +30,9 @@ public FlotillaDbContext CreateDbContext(string[] args) .AddEnvironmentVariables() .Build(); - string? keyVaultUri = config.GetSection("KeyVault")["VaultUri"] ?? throw new KeyNotFoundException("No key vault in config"); + string? keyVaultUri = + config.GetSection("KeyVault")["VaultUri"] + ?? throw new KeyNotFoundException("No key vault in config"); // Connect to keyvault var keyVault = new SecretClient( diff --git a/backend/api/Database/Context/FlotillaDbContext.cs b/backend/api/Database/Context/FlotillaDbContext.cs index 9aef1595f..3b793fac3 100644 --- a/backend/api/Database/Context/FlotillaDbContext.cs +++ b/backend/api/Database/Context/FlotillaDbContext.cs @@ -4,14 +4,17 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + namespace Api.Database.Context { public class FlotillaDbContext : DbContext { - public FlotillaDbContext(DbContextOptions options) : base(options) + public FlotillaDbContext(DbContextOptions options) + : base(options) { ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; } + public DbSet Robots => Set(); public DbSet RobotModels => Set(); public DbSet MissionRuns => Set(); @@ -24,14 +27,17 @@ public FlotillaDbContext(DbContextOptions options) : base(options) public DbSet Decks => Set(); public DbSet Areas => Set(); public DbSet Sources => Set(); - public DbSet DefaultLocalizationPoses => Set(); + public DbSet DefaultLocalizationPoses => + Set(); public DbSet AccessRoles => Set(); public DbSet UserInfos => Set(); public DbSet TagInspectionMetadata => Set(); // Timeseries: - public DbSet RobotPressureTimeseries => Set(); - public DbSet RobotBatteryTimeseries => Set(); + public DbSet RobotPressureTimeseries => + Set(); + public DbSet RobotBatteryTimeseries => + Set(); public DbSet RobotPoseTimeseries => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -42,11 +48,17 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities#collections-of-owned-types modelBuilder.Entity(missionRunEntity => { - if (isSqlLite) { AddConverterForDateTimeOffsets(ref missionRunEntity); } + if (isSqlLite) + { + AddConverterForDateTimeOffsets(ref missionRunEntity); + } }); modelBuilder.Entity(missionTaskEntity => { - if (isSqlLite) { AddConverterForDateTimeOffsets(ref missionTaskEntity); } + if (isSqlLite) + { + AddConverterForDateTimeOffsets(ref missionTaskEntity); + } missionTaskEntity.OwnsOne( task => task.RobotPose, poseEntity => @@ -57,45 +69,84 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ); }); - AddConverterForListOfEnums(modelBuilder.Entity() - .Property(r => r.RobotCapabilities)); + AddConverterForListOfEnums( + modelBuilder.Entity().Property(r => r.RobotCapabilities) + ); - modelBuilder.Entity() + modelBuilder + .Entity() .Property(m => m.InspectionFrequency) .HasConversion(new TimeSpanToTicksConverter()); - modelBuilder.Entity().OwnsOne(m => m.Map).OwnsOne(t => t.TransformationMatrices); + modelBuilder + .Entity() + .OwnsOne(m => m.Map) + .OwnsOne(t => t.TransformationMatrices); modelBuilder.Entity().OwnsOne(m => m.Map).OwnsOne(b => b.Boundary); - modelBuilder.Entity().HasOne(m => m.InspectionArea).WithMany().OnDelete(DeleteBehavior.Restrict); - modelBuilder.Entity().HasOne(m => m.InspectionArea).WithMany().OnDelete(DeleteBehavior.Restrict); + modelBuilder + .Entity() + .HasOne(m => m.InspectionArea) + .WithMany() + .OnDelete(DeleteBehavior.Restrict); + modelBuilder + .Entity() + .HasOne(m => m.InspectionArea) + .WithMany() + .OnDelete(DeleteBehavior.Restrict); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Orientation); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Position); - modelBuilder.Entity().OwnsOne(d => d.Pose, poseBuilder => - { - poseBuilder.OwnsOne(pose => pose.Position); - poseBuilder.OwnsOne(pose => pose.Orientation); - }); + modelBuilder + .Entity() + .OwnsOne( + d => d.Pose, + poseBuilder => + { + poseBuilder.OwnsOne(pose => pose.Position); + poseBuilder.OwnsOne(pose => pose.Orientation); + } + ); // There can only be one robot model per robot type modelBuilder.Entity().HasIndex(model => model.Type).IsUnique(); // There can only be one unique installation and plant shortname - modelBuilder.Entity().HasIndex(a => new - { - a.InstallationCode - }).IsUnique(); - modelBuilder.Entity().HasIndex(a => new - { - a.PlantCode - }).IsUnique(); - - modelBuilder.Entity().HasOne(a => a.Deck).WithMany().OnDelete(DeleteBehavior.Restrict); - modelBuilder.Entity().HasOne(a => a.Plant).WithMany().OnDelete(DeleteBehavior.Restrict); - modelBuilder.Entity().HasOne(a => a.Installation).WithMany().OnDelete(DeleteBehavior.Restrict); - modelBuilder.Entity().HasOne(d => d.Plant).WithMany().OnDelete(DeleteBehavior.Restrict); - modelBuilder.Entity().HasOne(d => d.Installation).WithMany().OnDelete(DeleteBehavior.Restrict); - modelBuilder.Entity().HasOne(p => p.Installation).WithMany().OnDelete(DeleteBehavior.Restrict); + modelBuilder + .Entity() + .HasIndex(a => new { a.InstallationCode }) + .IsUnique(); + modelBuilder.Entity().HasIndex(a => new { a.PlantCode }).IsUnique(); + + modelBuilder + .Entity() + .HasOne(a => a.Deck) + .WithMany() + .OnDelete(DeleteBehavior.Restrict); + modelBuilder + .Entity() + .HasOne(a => a.Plant) + .WithMany() + .OnDelete(DeleteBehavior.Restrict); + modelBuilder + .Entity() + .HasOne(a => a.Installation) + .WithMany() + .OnDelete(DeleteBehavior.Restrict); + modelBuilder + .Entity() + .HasOne(d => d.Plant) + .WithMany() + .OnDelete(DeleteBehavior.Restrict); + modelBuilder + .Entity() + .HasOne(d => d.Installation) + .WithMany() + .OnDelete(DeleteBehavior.Restrict); + modelBuilder + .Entity() + .HasOne(p => p.Installation) + .WithMany() + .OnDelete(DeleteBehavior.Restrict); } protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) @@ -106,12 +157,11 @@ protected override void ConfigureConventions(ModelConfigurationBuilder configura private static void AddConverterForDateTimeOffsets(ref EntityTypeBuilder entity) where T : class { - var properties = entity.Metadata.ClrType - .GetProperties() - .Where( - p => - p.PropertyType == typeof(DateTimeOffset) - || p.PropertyType == typeof(DateTimeOffset?) + var properties = entity + .Metadata.ClrType.GetProperties() + .Where(p => + p.PropertyType == typeof(DateTimeOffset) + || p.PropertyType == typeof(DateTimeOffset?) ); foreach (var property in properties) { @@ -119,19 +169,29 @@ private static void AddConverterForDateTimeOffsets(ref EntityTypeBuilder e } } - private static void AddConverterForListOfEnums(PropertyBuilder?> propertyBuilder) + private static void AddConverterForListOfEnums( + PropertyBuilder?> propertyBuilder + ) where T : Enum { #pragma warning disable IDE0305 var valueComparer = new ValueComparer?>( - (c1, c2) => (c1 == null && c2 == null) || ((c1 != null == (c2 != null)) && c1!.SequenceEqual(c2!)), - c => c == null ? 0 : c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), - c => c == null ? null : (IList?)c.ToList()); + (c1, c2) => + (c1 == null && c2 == null) + || ((c1 != null == (c2 != null)) && c1!.SequenceEqual(c2!)), + c => c == null ? 0 : c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())), + c => c == null ? null : (IList?)c.ToList() + ); #pragma warning restore IDE0305 - propertyBuilder.HasConversion( + propertyBuilder + .HasConversion( r => r != null ? string.Join(';', r) : "", - r => r.Split(';', StringSplitOptions.RemoveEmptyEntries).Select(r => (T)Enum.Parse(typeof(T), r)).ToList()) + r => + r.Split(';', StringSplitOptions.RemoveEmptyEntries) + .Select(r => (T)Enum.Parse(typeof(T), r)) + .ToList() + ) .Metadata.SetValueComparer(valueComparer); } } diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index c3d529ff0..2958a9649 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -1,6 +1,7 @@ using Api.Database.Models; using Microsoft.EntityFrameworkCore; using TaskStatus = Api.Database.Models.TaskStatus; + namespace Api.Database.Context { public static class InitDb @@ -13,27 +14,18 @@ public static class InitDb private static readonly List areas = GetAreas(); private static readonly List sources = GetSources(); private static readonly List tasks = GetMissionTasks(); - private static readonly List missionDefinitions = GetMissionDefinitions(); + private static readonly List missionDefinitions = + GetMissionDefinitions(); private static readonly List missionRuns = GetMissionRuns(); private static readonly List accessRoles = GetAccessRoles(); private static List GetInspections() { - var inspection1 = new Inspection - { - InspectionType = InspectionType.Image - }; + var inspection1 = new Inspection { InspectionType = InspectionType.Image }; - var inspection2 = new Inspection - { - InspectionType = InspectionType.ThermalImage - }; + var inspection2 = new Inspection { InspectionType = InspectionType.ThermalImage }; - return new List( - [ - inspection1, - inspection2 - ]); + return new List([inspection1, inspection2]); } private static List GetAccessRoles() @@ -42,13 +34,10 @@ private static List GetAccessRoles() { Installation = installations[0], AccessLevel = RoleAccessLevel.ADMIN, - RoleName = "Role.User.HUA" + RoleName = "Role.User.HUA", }; - return new List( - [ - accessRole1 - ]); + return new List([accessRole1]); } private static List GetInstallations() @@ -57,21 +46,17 @@ private static List GetInstallations() { Id = Guid.NewGuid().ToString(), Name = "Huldra", - InstallationCode = "HUA" + InstallationCode = "HUA", }; var installation2 = new Installation { Id = Guid.NewGuid().ToString(), Name = "Kårstø", - InstallationCode = "KAA" + InstallationCode = "KAA", }; - return new List( - [ - installation1, - installation2 - ]); + return new List([installation1, installation2]); } private static List GetPlants() @@ -81,7 +66,7 @@ private static List GetPlants() Id = Guid.NewGuid().ToString(), Installation = installations[0], Name = "HULDRA", - PlantCode = "HUA" + PlantCode = "HUA", }; var plant2 = new Plant @@ -89,14 +74,10 @@ private static List GetPlants() Id = Guid.NewGuid().ToString(), Installation = installations[1], Name = "Kårstø", - PlantCode = "Kårstø" + PlantCode = "Kårstø", }; - return new List( - [ - plant1, - plant2 - ]); + return new List([plant1, plant2]); } private static List GetDecks() @@ -107,7 +88,7 @@ private static List GetDecks() Plant = plants[0], Installation = plants[0].Installation, DefaultLocalizationPose = new DefaultLocalizationPose(), - Name = "TestDeck" + Name = "TestDeck", }; var deck2 = new Deck @@ -116,7 +97,7 @@ private static List GetDecks() Plant = plants[0], Installation = plants[0].Installation, DefaultLocalizationPose = new DefaultLocalizationPose(), - Name = "TestDeck2" + Name = "TestDeck2", }; var deck3 = new Deck @@ -125,7 +106,7 @@ private static List GetDecks() Plant = plants[0], Installation = plants[0].Installation, DefaultLocalizationPose = new DefaultLocalizationPose(), - Name = "TestDeck3" + Name = "TestDeck3", }; var deck4 = new Deck @@ -134,7 +115,7 @@ private static List GetDecks() Plant = plants[0], Installation = plants[0].Installation, DefaultLocalizationPose = new DefaultLocalizationPose(), - Name = "TestDeck4" + Name = "TestDeck4", }; var deckHuldraMezzanine = new Deck @@ -143,7 +124,7 @@ private static List GetDecks() Plant = plants[0], Installation = plants[0].Installation, DefaultLocalizationPose = new DefaultLocalizationPose(), - Name = "Huldra Mezzanine Deck" + Name = "Huldra Mezzanine Deck", }; var deckKLab = new Deck @@ -152,18 +133,10 @@ private static List GetDecks() Plant = plants[1], Installation = plants[1].Installation, DefaultLocalizationPose = new DefaultLocalizationPose(), - Name = "K-Lab" - }; - - return new List( - [ - deck1, - deck2, - deck3, - deck4, - deckHuldraMezzanine, - deckKLab - ]); + Name = "K-Lab", + }; + + return new List([deck1, deck2, deck3, deck4, deckHuldraMezzanine, deckKLab]); } private static List GetAreas() @@ -257,41 +230,19 @@ private static List GetAreas() }; return new List( - [ - area1, - area2, - area3, - area4, - area5, - area6, - areaHuldraHB, - areaKLab - ]); + [area1, area2, area3, area4, area5, area6, areaHuldraHB, areaKLab] + ); } private static List GetSources() { - var source1 = new Source - { - SourceId = "986", - }; + var source1 = new Source { SourceId = "986" }; - var source2 = new Source - { - SourceId = "990", - }; + var source2 = new Source { SourceId = "990" }; - var source3 = new Source - { - SourceId = "991", - }; + var source3 = new Source { SourceId = "991" }; - return new List( - [ - source1, - source2, - source3 - ]); + return new List([source1, source2, source3]); } private static List GetRobots() @@ -306,7 +257,7 @@ private static List GetRobots() Port = 3000, CurrentInstallation = installations[0], Documentation = [], - Pose = new Pose() + Pose = new Pose(), }; var robot2 = new Robot @@ -319,7 +270,7 @@ private static List GetRobots() Port = 3000, CurrentInstallation = installations[0], Documentation = [], - Pose = new Pose() + Pose = new Pose(), }; var robot3 = new Robot @@ -332,15 +283,10 @@ private static List GetRobots() Port = 3000, CurrentInstallation = installations[0], Documentation = [], - Pose = new Pose() + Pose = new Pose(), }; - return new List( - [ - robot1, - robot2, - robot3 - ]); + return new List([robot1, robot2, robot3]); } private static List GetMissionDefinitions() @@ -354,7 +300,7 @@ private static List GetMissionDefinitions() Source = sources[0], Comment = "Interesting comment", InspectionFrequency = new DateTime().AddDays(12) - new DateTime(), - LastSuccessfulRun = null + LastSuccessfulRun = null, }; var missionDefinition2 = new MissionDefinition @@ -365,7 +311,7 @@ private static List GetMissionDefinitions() InspectionArea = decks[1], Source = sources[1], InspectionFrequency = new DateTime().AddDays(7) - new DateTime(), - LastSuccessfulRun = null + LastSuccessfulRun = null, }; var missionDefinition3 = new MissionDefinition @@ -375,7 +321,7 @@ private static List GetMissionDefinitions() InstallationCode = decks[1].Installation!.InstallationCode, InspectionArea = decks[1], Source = sources[2], - LastSuccessfulRun = null + LastSuccessfulRun = null, }; var missionDefinition4 = new MissionDefinition @@ -386,7 +332,7 @@ private static List GetMissionDefinitions() InspectionFrequency = new DateTime().AddDays(90) - new DateTime(), InspectionArea = decks[2], Source = sources[2], - LastSuccessfulRun = null + LastSuccessfulRun = null, }; var missionDefinition5 = new MissionDefinition @@ -397,7 +343,7 @@ private static List GetMissionDefinitions() InspectionFrequency = new DateTime().AddDays(35) - new DateTime(), InspectionArea = decks[2], Source = sources[2], - LastSuccessfulRun = null + LastSuccessfulRun = null, }; var missionDefinition6 = new MissionDefinition @@ -408,7 +354,7 @@ private static List GetMissionDefinitions() InspectionFrequency = new DateTime().AddDays(4) - new DateTime(), InspectionArea = decks[3], Source = sources[2], - LastSuccessfulRun = null + LastSuccessfulRun = null, }; _ = new MissionDefinition { @@ -417,25 +363,24 @@ private static List GetMissionDefinitions() InstallationCode = decks[3].Installation.InstallationCode, InspectionArea = decks[4], Source = sources[2], - LastSuccessfulRun = null + LastSuccessfulRun = null, }; return new List( - [ - missionDefinition1, - missionDefinition2, - missionDefinition3, - missionDefinition4, - missionDefinition5, - missionDefinition6 - ]); + [ + missionDefinition1, + missionDefinition2, + missionDefinition3, + missionDefinition4, + missionDefinition5, + missionDefinition6, + ] + ); } private static List GetMissionTasks() { - var url = new Uri( - "https://stid.equinor.com/hua/tag?tagNo=ABCD" - ); + var url = new Uri("https://stid.equinor.com/hua/tag?tagNo=ABCD"); var task1 = new MissionTask( inspection: new Inspection(), robotPose: new Pose(300.0f, 50.0f, 200.0f, 0.0f, 0.0f, 0.0f, 1.0f), @@ -513,15 +458,7 @@ private static List GetMissionTasks() status: TaskStatus.Failed ); - return [ - task1, - task2, - task3, - task4, - task5, - task6, - task7 - ]; + return [task1, task2, task3, task4, task5, task6, task7]; } private static List GetMissionRuns() @@ -535,7 +472,7 @@ private static List GetMissionRuns() MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, DesiredStartTime = DateTime.UtcNow, - Tasks = [] + Tasks = [], }; var missionRun2 = new MissionRun @@ -547,7 +484,7 @@ private static List GetMissionRuns() MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, DesiredStartTime = DateTime.UtcNow, - Tasks = [] + Tasks = [], }; missionDefinitions[0].LastSuccessfulRun = missionRun2; @@ -560,7 +497,7 @@ private static List GetMissionRuns() MissionId = missionDefinitions[1].Id, Status = MissionStatus.Successful, DesiredStartTime = DateTime.UtcNow, - Tasks = [] + Tasks = [], }; var missionRun4 = new MissionRun @@ -572,11 +509,7 @@ private static List GetMissionRuns() MissionId = missionDefinitions[1].Id, Status = MissionStatus.Failed, DesiredStartTime = DateTime.UtcNow, - Tasks = - [ - tasks[0], - tasks[1] - ] + Tasks = [tasks[0], tasks[1]], }; var missionRun5 = new MissionRun @@ -588,11 +521,7 @@ private static List GetMissionRuns() MissionId = missionDefinitions[1].Id, Status = MissionStatus.PartiallySuccessful, DesiredStartTime = DateTime.UtcNow, - Tasks = - [ - tasks[0], - tasks[2] - ] + Tasks = [tasks[0], tasks[2]], }; var missionRun6 = new MissionRun @@ -604,11 +533,7 @@ private static List GetMissionRuns() MissionId = missionDefinitions[1].Id, Status = MissionStatus.Cancelled, DesiredStartTime = DateTime.UtcNow, - Tasks = - [ - tasks[0], - tasks[3] - ] + Tasks = [tasks[0], tasks[3]], }; var missionRun7 = new MissionRun @@ -620,30 +545,22 @@ private static List GetMissionRuns() MissionId = missionDefinitions[1].Id, Status = MissionStatus.Failed, DesiredStartTime = DateTime.UtcNow, - Tasks = - [ - tasks[0], - tasks[1], - tasks[2], - tasks[3], - tasks[4], - tasks[5], - tasks[6] - ] + Tasks = [tasks[0], tasks[1], tasks[2], tasks[3], tasks[4], tasks[5], tasks[6]], }; missionDefinitions[1].LastSuccessfulRun = missionRun3; return new List( - [ - missionRun1, - missionRun2, - missionRun3, - missionRun4, - missionRun5, - missionRun6, - missionRun7 - ]); + [ + missionRun1, + missionRun2, + missionRun3, + missionRun4, + missionRun5, + missionRun6, + missionRun7, + ] + ); } public static void AddRobotModelsToContext(FlotillaDbContext context) @@ -657,7 +574,7 @@ public static void AddRobotModelsToContext(FlotillaDbContext context) BatteryWarningThreshold = null, BatteryMissionStartThreshold = null, LowerPressureWarningThreshold = null, - UpperPressureWarningThreshold = null + UpperPressureWarningThreshold = null, }; context.Add(model); } diff --git a/backend/api/Database/Models/AccessRole.cs b/backend/api/Database/Models/AccessRole.cs index aa0c5fedb..169013a0b 100644 --- a/backend/api/Database/Models/AccessRole.cs +++ b/backend/api/Database/Models/AccessRole.cs @@ -8,7 +8,7 @@ public enum RoleAccessLevel { READ_ONLY, USER, - ADMIN + ADMIN, } public class AccessRole diff --git a/backend/api/Database/Models/Area.cs b/backend/api/Database/Models/Area.cs index d2db416dd..1ea054b59 100644 --- a/backend/api/Database/Models/Area.cs +++ b/backend/api/Database/Models/Area.cs @@ -27,6 +27,5 @@ public class Area public MapMetadata MapMetadata { get; set; } public DefaultLocalizationPose? DefaultLocalizationPose { get; set; } - } } diff --git a/backend/api/Database/Models/Inspection.cs b/backend/api/Database/Models/Inspection.cs index 8bcd2a34b..c77e7be53 100644 --- a/backend/api/Database/Models/Inspection.cs +++ b/backend/api/Database/Models/Inspection.cs @@ -23,7 +23,7 @@ public Inspection( string? inspectionTargetName, InspectionStatus status = InspectionStatus.NotStarted, AnalysisType? analysisType = null - ) + ) { InspectionType = inspectionType; VideoDuration = videoDuration; @@ -43,7 +43,11 @@ public Inspection(CustomInspectionQuery inspectionQuery) } // Creates a blank deepcopy of the provided inspection - public Inspection(Inspection copy, InspectionStatus? inspectionStatus = null, bool useEmptyID = false) + public Inspection( + Inspection copy, + InspectionStatus? inspectionStatus = null, + bool useEmptyID = false + ) { Id = useEmptyID ? "" : Guid.NewGuid().ToString(); IsarTaskId = useEmptyID ? "" : copy.IsarTaskId; @@ -96,8 +100,8 @@ public InspectionStatus Status public bool IsCompleted => _status is InspectionStatus.Cancelled - or InspectionStatus.Successful - or InspectionStatus.Failed; + or InspectionStatus.Successful + or InspectionStatus.Failed; [Required] public InspectionType InspectionType { get; set; } @@ -125,13 +129,15 @@ public void UpdateWithIsarInfo(IsarTask isarTask) IsarTaskType.TakeThermalImage => InspectionType.ThermalImage, IsarTaskType.TakeVideo => InspectionType.Video, IsarTaskType.TakeThermalVideo => InspectionType.ThermalVideo, - _ - => throw new ArgumentException( - $"ISAR task type '{isarTask.TaskType}' not supported for inspections" - ) + _ => throw new ArgumentException( + $"ISAR task type '{isarTask.TaskType}' not supported for inspections" + ), }; IsarTaskId = isarTask.IsarTaskId; - if (isarTask.IsarInspectionId != null) { IsarInspectionId = isarTask.IsarInspectionId; } + if (isarTask.IsarInspectionId != null) + { + IsarInspectionId = isarTask.IsarInspectionId; + } } public void UpdateStatus(IsarTaskStatus isarStatus) @@ -144,10 +150,9 @@ public void UpdateStatus(IsarTaskStatus isarStatus) IsarTaskStatus.Cancelled => InspectionStatus.Cancelled, IsarTaskStatus.Failed => InspectionStatus.Failed, IsarTaskStatus.Paused => InspectionStatus.InProgress, - _ - => throw new ArgumentException( - $"ISAR task status '{isarStatus}' not supported for inspection status" - ) + _ => throw new ArgumentException( + $"ISAR task status '{isarStatus}' not supported for inspection status" + ), }; } @@ -156,9 +161,13 @@ public bool IsSupportedInspectionType(IList capabilities) return InspectionType switch { InspectionType.Image => capabilities.Contains(RobotCapabilitiesEnum.take_image), - InspectionType.ThermalImage => capabilities.Contains(RobotCapabilitiesEnum.take_thermal_image), + InspectionType.ThermalImage => capabilities.Contains( + RobotCapabilitiesEnum.take_thermal_image + ), InspectionType.Video => capabilities.Contains(RobotCapabilitiesEnum.take_video), - InspectionType.ThermalVideo => capabilities.Contains(RobotCapabilitiesEnum.take_thermal_video), + InspectionType.ThermalVideo => capabilities.Contains( + RobotCapabilitiesEnum.take_thermal_video + ), InspectionType.Audio => capabilities.Contains(RobotCapabilitiesEnum.record_audio), _ => false, }; @@ -171,7 +180,7 @@ public enum InspectionStatus InProgress, NotStarted, Failed, - Cancelled + Cancelled, } public enum InspectionType @@ -180,12 +189,12 @@ public enum InspectionType ThermalImage, Video, ThermalVideo, - Audio + Audio, } public enum AnalysisType { CarSeal, - RtjFlange + RtjFlange, } } diff --git a/backend/api/Database/Models/InspectionFinding.cs b/backend/api/Database/Models/InspectionFinding.cs index 5d63c470b..444dc1e46 100644 --- a/backend/api/Database/Models/InspectionFinding.cs +++ b/backend/api/Database/Models/InspectionFinding.cs @@ -32,5 +32,4 @@ public InspectionFinding() Finding = "string"; } } - } diff --git a/backend/api/Database/Models/MapMetadata.cs b/backend/api/Database/Models/MapMetadata.cs index e67e9e199..65d1e55f7 100644 --- a/backend/api/Database/Models/MapMetadata.cs +++ b/backend/api/Database/Models/MapMetadata.cs @@ -85,7 +85,11 @@ public Boundary(double x1, double y1, double x2, double y2, double z1, double z2 public List As2DMatrix() { - return [[X1, Y1], [X2, Y2]]; + return + [ + [X1, Y1], + [X2, Y2], + ]; } } } diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index c2596830c..c0f6307f8 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -74,10 +74,10 @@ public IList Tasks public bool IsCompleted => _status is MissionStatus.Aborted - or MissionStatus.Cancelled - or MissionStatus.Successful - or MissionStatus.PartiallySuccessful - or MissionStatus.Failed; + or MissionStatus.Cancelled + or MissionStatus.Successful + or MissionStatus.PartiallySuccessful + or MissionStatus.Failed; public DateTime? StartTime { get; private set; } @@ -106,10 +106,9 @@ public void UpdateWithIsarInfo(IsarMission isarMission) public MissionTask? GetTaskByIsarId(string isarTaskId) { - return Tasks.FirstOrDefault( - task => - task.IsarTaskId != null - && task.IsarTaskId.Equals(isarTaskId, StringComparison.Ordinal) + return Tasks.FirstOrDefault(task => + task.IsarTaskId != null + && task.IsarTaskId.Equals(isarTaskId, StringComparison.Ordinal) ); } @@ -124,10 +123,9 @@ public static MissionStatus GetMissionStatusFromString(string status) "cancelled" => MissionStatus.Cancelled, "paused" => MissionStatus.Paused, "partially_successful" => MissionStatus.PartiallySuccessful, - _ - => throw new ArgumentException( - $"Failed to parse mission status '{status}' as it's not supported" - ) + _ => throw new ArgumentException( + $"Failed to parse mission status '{status}' as it's not supported" + ), }; } @@ -135,8 +133,8 @@ public void CalculateEstimatedDuration() { if (Robot.Model.AverageDurationPerTag is not null) { - float totalInspectionDuration = Tasks.Sum( - task => task.Inspection?.VideoDuration ?? 0 + float totalInspectionDuration = Tasks.Sum(task => + task.Inspection?.VideoDuration ?? 0 ); EstimatedDuration = (uint)( (Robot.Model.AverageDurationPerTag * Tasks.Count) + totalInspectionDuration @@ -164,16 +162,21 @@ public void CalculateEstimatedDuration() prevPosition = currentPosition; } int estimate = (int)( - (distance / (RobotVelocity * EfficiencyFactor)) - + InspectionTime + (distance / (RobotVelocity * EfficiencyFactor)) + InspectionTime ); EstimatedDuration = (uint)estimate * 60; } } - public bool IsReturnHomeMission() { return MissionRunType == MissionRunType.ReturnHome; } + public bool IsReturnHomeMission() + { + return MissionRunType == MissionRunType.ReturnHome; + } - public bool IsEmergencyMission() { return MissionRunType == MissionRunType.Emergency; } + public bool IsEmergencyMission() + { + return MissionRunType == MissionRunType.Emergency; + } } public enum MissionStatus @@ -185,13 +188,13 @@ public enum MissionStatus Cancelled, Failed, Successful, - PartiallySuccessful + PartiallySuccessful, } public enum MissionRunType { Normal, ReturnHome, - Emergency + Emergency, } } diff --git a/backend/api/Database/Models/MissionTask.cs b/backend/api/Database/Models/MissionTask.cs index c4e290107..8c6c69ca4 100644 --- a/backend/api/Database/Models/MissionTask.cs +++ b/backend/api/Database/Models/MissionTask.cs @@ -11,7 +11,6 @@ namespace Api.Database.Models { public class MissionTask { - private TaskStatus _status; // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized @@ -28,7 +27,8 @@ public MissionTask( string? taskDescription, IsarZoomDescription? zoomDescription = null, TaskStatus status = TaskStatus.NotStarted, - MissionTaskType type = MissionTaskType.Inspection) + MissionTaskType type = MissionTaskType.Inspection + ) { TagLink = tagLink; TagId = tagId; @@ -39,7 +39,8 @@ public MissionTask( Status = status; Type = type; IsarZoomDescription = zoomDescription; - if (inspection != null) Inspection = new Inspection(inspection); + if (inspection != null) + Inspection = new Inspection(inspection); } public MissionTask(CustomTaskQuery taskQuery) @@ -81,7 +82,9 @@ public MissionTask(Pose robotPose, MissionTaskType type) Inspection = new Inspection(); break; default: - throw new MissionTaskNotFoundException("MissionTaskType should be ReturnHome or Inspection"); + throw new MissionTaskNotFoundException( + "MissionTaskType should be ReturnHome or Inspection" + ); } } @@ -138,18 +141,24 @@ public TaskStatus Status set { _status = value; - if (IsCompleted && EndTime is null) { EndTime = DateTime.UtcNow; } + if (IsCompleted && EndTime is null) + { + EndTime = DateTime.UtcNow; + } - if (_status is TaskStatus.InProgress && StartTime is null) { StartTime = DateTime.UtcNow; } + if (_status is TaskStatus.InProgress && StartTime is null) + { + StartTime = DateTime.UtcNow; + } } } public bool IsCompleted => _status is TaskStatus.Cancelled - or TaskStatus.Successful - or TaskStatus.Failed - or TaskStatus.PartiallySuccessful; + or TaskStatus.Successful + or TaskStatus.Failed + or TaskStatus.PartiallySuccessful; public DateTime? StartTime { get; private set; } @@ -162,7 +171,10 @@ or TaskStatus.Failed public void UpdateWithIsarInfo(IsarTask isarTask) { UpdateStatus(isarTask.TaskStatus); - if (isarTask.TaskType != IsarTaskType.ReturnToHome && isarTask.TaskType != IsarTaskType.MoveArm) + if ( + isarTask.TaskType != IsarTaskType.ReturnToHome + && isarTask.TaskType != IsarTaskType.MoveArm + ) { Inspection?.UpdateWithIsarInfo(isarTask); } @@ -179,7 +191,7 @@ public void UpdateStatus(IsarTaskStatus isarStatus) IsarTaskStatus.Cancelled => TaskStatus.Cancelled, IsarTaskStatus.Paused => TaskStatus.Paused, IsarTaskStatus.Failed => TaskStatus.Failed, - _ => throw new ArgumentException($"ISAR Task status '{isarStatus}' not supported") + _ => throw new ArgumentException($"ISAR Task status '{isarStatus}' not supported"), }; } @@ -189,7 +201,9 @@ public static string ConvertMissionTaskTypeToIsarTaskType(MissionTaskType missio { MissionTaskType.ReturnHome => "return_to_home", MissionTaskType.Inspection => "inspection", - _ => throw new ArgumentException($"ISAR Mission task type '{missionTaskType}' not supported"), + _ => throw new ArgumentException( + $"ISAR Mission task type '{missionTaskType}' not supported" + ), }; ; } @@ -199,19 +213,19 @@ public static string CalculateHashFromTasks(IList tasks) var genericTasks = new List(); foreach (var task in tasks) { - var taskCopy = new MissionTask(task) - { - Id = "", - IsarTaskId = "", - }; - if (taskCopy.Inspection is not null) taskCopy.Inspection = new Inspection(taskCopy.Inspection, useEmptyID: true); + var taskCopy = new MissionTask(task) { Id = "", IsarTaskId = "" }; + if (taskCopy.Inspection is not null) + taskCopy.Inspection = new Inspection(taskCopy.Inspection, useEmptyID: true); genericTasks.Add(taskCopy); } string json = JsonSerializer.Serialize(genericTasks); byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); - return BitConverter.ToString(hash).Replace("-", "", StringComparison.CurrentCulture).ToUpperInvariant(); + return BitConverter + .ToString(hash) + .Replace("-", "", StringComparison.CurrentCulture) + .ToUpperInvariant(); } } @@ -223,12 +237,12 @@ public enum TaskStatus InProgress, Failed, Cancelled, - Paused + Paused, } public enum MissionTaskType { Inspection, - ReturnHome + ReturnHome, } } diff --git a/backend/api/Database/Models/Pose.cs b/backend/api/Database/Models/Pose.cs index aa1a49900..d2a6b0ee0 100644 --- a/backend/api/Database/Models/Pose.cs +++ b/backend/api/Database/Models/Pose.cs @@ -3,6 +3,7 @@ using Api.Mqtt.MessageModels; using Api.Services.Models; using Microsoft.EntityFrameworkCore; + namespace Api.Database.Models { [Owned] @@ -34,21 +35,39 @@ public Orientation(float x = 0, float y = 0, float z = 0, float w = 1) [Required] public float X { get; set; } + [Required] public float Y { get; set; } + [Required] public float Z { get; set; } + [Required] public float W { get; set; } public override bool Equals(object obj) { - if (obj is not Orientation orientation) { return false; } + if (obj is not Orientation orientation) + { + return false; + } const float Tolerance = 1e-6F; - if (MathF.Abs(orientation.X - X) > Tolerance) { return false; } - if (MathF.Abs(orientation.Y - Y) > Tolerance) { return false; } - if (MathF.Abs(orientation.Z - Z) > Tolerance) { return false; } - if (MathF.Abs(orientation.W - W) > Tolerance) { return false; } + if (MathF.Abs(orientation.X - X) > Tolerance) + { + return false; + } + if (MathF.Abs(orientation.Y - Y) > Tolerance) + { + return false; + } + if (MathF.Abs(orientation.Z - Z) > Tolerance) + { + return false; + } + if (MathF.Abs(orientation.W - W) > Tolerance) + { + return false; + } return true; } @@ -62,7 +81,6 @@ public override int GetHashCode() [Owned] public class Position { - public Position() { X = 0; @@ -86,19 +104,33 @@ public Position(float x = 0, float y = 0, float z = 0) [Required] public float X { get; set; } + [Required] public float Y { get; set; } + [Required] public float Z { get; set; } public override bool Equals(object obj) { - if (obj is not Position position) { return false; } + if (obj is not Position position) + { + return false; + } const float Tolerance = 1e-6F; - if (MathF.Abs(position.X - X) > Tolerance) { return false; } - if (MathF.Abs(position.Y - Y) > Tolerance) { return false; } - if (MathF.Abs(position.Z - Z) > Tolerance) { return false; } + if (MathF.Abs(position.X - X) > Tolerance) + { + return false; + } + if (MathF.Abs(position.Y - Y) > Tolerance) + { + return false; + } + if (MathF.Abs(position.Z - Z) > Tolerance) + { + return false; + } return true; } @@ -112,7 +144,6 @@ public override int GetHashCode() [Owned] public class Pose { - public Pose() { Position = new Position(); @@ -128,7 +159,12 @@ public Pose(Pose copy) public Pose(IsarPoseMqtt isarPose) { Position = new Position(isarPose.Position.X, isarPose.Position.Y, isarPose.Position.Z); - Orientation = new Orientation(isarPose.Orientation.X, isarPose.Orientation.Y, isarPose.Orientation.Z, isarPose.Orientation.W); + Orientation = new Orientation( + isarPose.Orientation.X, + isarPose.Orientation.Y, + isarPose.Orientation.Z, + isarPose.Orientation.W + ); } public Pose( @@ -156,6 +192,7 @@ public Pose(Position position, Orientation orientation) Position = position; Orientation = orientation; } + [Required] public Position Position { get; set; } @@ -171,7 +208,10 @@ public Orientation AxisAngleToQuaternion(float echoAngle) float angle; echoAngle %= 2F * MathF.PI; - if (echoAngle < 0) { echoAngle += 2F * MathF.PI; } + if (echoAngle < 0) + { + echoAngle += 2F * MathF.PI; + } angle = (450 * MathF.PI / 180) - echoAngle; @@ -180,7 +220,7 @@ public Orientation AxisAngleToQuaternion(float echoAngle) X = 0, Y = 0, Z = MathF.Sin(angle / 2), - W = MathF.Cos(angle / 2) + W = MathF.Cos(angle / 2), }; return quaternion; diff --git a/backend/api/Database/Models/Robot.cs b/backend/api/Database/Models/Robot.cs index 0fce2d7a6..c8fd712c6 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -20,7 +20,12 @@ public Robot() Pose = new Pose(); } - public Robot(CreateRobotQuery createQuery, Installation installation, RobotModel model, Deck? inspectionArea = null) + public Robot( + CreateRobotQuery createQuery, + Installation installation, + RobotModel model, + Deck? inspectionArea = null + ) { var documentation = new List(); foreach (var documentQuery in createQuery.Documentation) @@ -28,7 +33,7 @@ public Robot(CreateRobotQuery createQuery, Installation installation, RobotModel var document = new DocumentInfo { Name = documentQuery.Name, - Url = documentQuery.Url + Url = documentQuery.Url, }; documentation.Add(document); } @@ -48,6 +53,7 @@ public Robot(CreateRobotQuery createQuery, Installation installation, RobotModel Pose = new Pose(); Model = model; } + [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } @@ -80,26 +86,40 @@ public Robot(CreateRobotQuery createQuery, Installation installation, RobotModel public bool IsRobotPressureTooLow() { - if (Model.LowerPressureWarningThreshold == null) { return false; } + if (Model.LowerPressureWarningThreshold == null) + { + return false; + } return PressureLevel == null || Model.LowerPressureWarningThreshold >= PressureLevel; } public bool IsRobotPressureTooHigh() { - if (Model.UpperPressureWarningThreshold == null) { return false; } + if (Model.UpperPressureWarningThreshold == null) + { + return false; + } return PressureLevel == null || Model.UpperPressureWarningThreshold <= PressureLevel; } public bool IsRobotBatteryTooLow() { - if (Model.BatteryWarningThreshold == null) { return false; } + if (Model.BatteryWarningThreshold == null) + { + return false; + } return Model.BatteryWarningThreshold >= BatteryLevel; } public bool IsRobotReadyToStartMissions() { - if (IsRobotBatteryTooLow()) return false; - if (Model.BatteryMissionStartThreshold != null && Model.BatteryMissionStartThreshold > BatteryLevel) return false; + if (IsRobotBatteryTooLow()) + return false; + if ( + Model.BatteryMissionStartThreshold != null + && Model.BatteryMissionStartThreshold > BatteryLevel + ) + return false; return !IsRobotPressureTooHigh() && !IsRobotPressureTooLow(); } diff --git a/backend/api/Database/Models/RobotModel.cs b/backend/api/Database/Models/RobotModel.cs index 09d61b4a2..cc4026f0e 100644 --- a/backend/api/Database/Models/RobotModel.cs +++ b/backend/api/Database/Models/RobotModel.cs @@ -19,12 +19,11 @@ public enum RobotType Robot, Turtlebot, AnymalX, - AnymalD + AnymalD, } public class RobotModel { - public RobotModel() { } public RobotModel(CreateRobotModelQuery query) @@ -34,6 +33,7 @@ public RobotModel(CreateRobotModelQuery query) UpperPressureWarningThreshold = query.UpperPressureWarningThreshold; LowerPressureWarningThreshold = query.LowerPressureWarningThreshold; } + [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } diff --git a/backend/api/Database/Models/TransformationMatrices.cs b/backend/api/Database/Models/TransformationMatrices.cs index 5805e536e..8bac495fa 100644 --- a/backend/api/Database/Models/TransformationMatrices.cs +++ b/backend/api/Database/Models/TransformationMatrices.cs @@ -48,7 +48,11 @@ public TransformationMatrices(double[] p1, double[] p2, int imageWidth, int imag public List AsMatrix() { - return [[C1, C2], [D1, D2]]; + return + [ + [C1, C2], + [D1, D2], + ]; } } } diff --git a/backend/api/Database/Models/UserInfo.cs b/backend/api/Database/Models/UserInfo.cs index 10f14f741..478c8ed22 100644 --- a/backend/api/Database/Models/UserInfo.cs +++ b/backend/api/Database/Models/UserInfo.cs @@ -9,6 +9,7 @@ public class UserInfo [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } + [Required] public string Oid { get; set; } } diff --git a/backend/api/EventHandlers/InspectionFindingEventHandler.cs b/backend/api/EventHandlers/InspectionFindingEventHandler.cs index 3d508a3fa..cf9f17c69 100644 --- a/backend/api/EventHandlers/InspectionFindingEventHandler.cs +++ b/backend/api/EventHandlers/InspectionFindingEventHandler.cs @@ -12,22 +12,34 @@ namespace Api.EventHandlers { - public class InspectionFindingEventHandler(IConfiguration configuration, - IServiceScopeFactory scopeFactory, - ILogger logger) : BackgroundService + public class InspectionFindingEventHandler( + IConfiguration configuration, + IServiceScopeFactory scopeFactory, + ILogger logger + ) : BackgroundService { private readonly string _cronExpression = "19 14 * * * "; - private InspectionFindingService InspectionFindingService => scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private readonly TimeSpan _timeSpan = configuration.GetValue("InspectionFindingEventHandler:TimeSpan"); + private InspectionFindingService InspectionFindingService => + scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); + private readonly TimeSpan _timeSpan = configuration.GetValue( + "InspectionFindingEventHandler:TimeSpan" + ); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - logger.LogInformation("InspectionFinding EventHandler service started at {time}", DateTime.UtcNow); + logger.LogInformation( + "InspectionFinding EventHandler service started at {time}", + DateTime.UtcNow + ); while (!stoppingToken.IsCancellationRequested) { var now = DateTime.UtcNow; - var nextExecutionTime = CrontabSchedule.Parse(_cronExpression).GetNextOccurrence(now); + var nextExecutionTime = CrontabSchedule + .Parse(_cronExpression) + .GetNextOccurrence(now); var delay = nextExecutionTime - now; if (delay.TotalMilliseconds > 0) @@ -35,38 +47,65 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) var lastReportingTime = DateTime.UtcNow - _timeSpan; - var inspectionFindings = await InspectionFindingService.RetrieveInspectionFindings(lastReportingTime, readOnly: true); - logger.LogInformation("Found {count} inspection findings in last {interval}", inspectionFindings.Count, _timeSpan); + var inspectionFindings = await InspectionFindingService.RetrieveInspectionFindings( + lastReportingTime, + readOnly: true + ); + logger.LogInformation( + "Found {count} inspection findings in last {interval}", + inspectionFindings.Count, + _timeSpan + ); if (inspectionFindings.Count > 0) { var findingsList = await GenerateFindingsList(inspectionFindings); - string adaptiveCardJson = GenerateAdaptiveCard($"Rapport {DateTime.UtcNow:yyyy-MM-dd HH}", inspectionFindings.Count, findingsList); + string adaptiveCardJson = GenerateAdaptiveCard( + $"Rapport {DateTime.UtcNow:yyyy-MM-dd HH}", + inspectionFindings.Count, + findingsList + ); string url = GetWebhookURL(configuration, "TeamsInspectionFindingsWebhook"); var client = new HttpClient(); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json") + ); - var content = new StringContent(adaptiveCardJson, Encoding.UTF8, "application/json"); + var content = new StringContent( + adaptiveCardJson, + Encoding.UTF8, + "application/json" + ); var response = await client.PostAsync(url, content, stoppingToken); if (response.StatusCode != HttpStatusCode.OK) { string errorBody = await response.Content.ReadAsStringAsync(stoppingToken); - logger.LogWarning($"Webhook request failed with status code {response.StatusCode}. Response body: {errorBody}"); + logger.LogWarning( + $"Webhook request failed with status code {response.StatusCode}. Response body: {errorBody}" + ); } } } } - private async Task> GenerateFindingsList(List inspectionFindings) + private async Task> GenerateFindingsList( + List inspectionFindings + ) { var findingsList = new List(); foreach (var inspectionFinding in inspectionFindings) { - var missionRun = await InspectionFindingService.GetMissionRunByIsarInspectionId(inspectionFinding.IsarTaskId, readOnly: true); - var task = await InspectionFindingService.GetMissionTaskByIsarInspectionId(inspectionFinding.IsarTaskId, readOnly: true); + var missionRun = await InspectionFindingService.GetMissionRunByIsarInspectionId( + inspectionFinding.IsarTaskId, + readOnly: true + ); + var task = await InspectionFindingService.GetMissionTaskByIsarInspectionId( + inspectionFinding.IsarTaskId, + readOnly: true + ); if (task != null && missionRun != null) { @@ -84,36 +123,79 @@ private async Task> GenerateFindingsList(List i return findingsList; } - public static string GenerateAdaptiveCard(string title, int numberOfFindings, List findingsReports) + public static string GenerateAdaptiveCard( + string title, + int numberOfFindings, + List findingsReports + ) { var findingsJsonArray = new JArray(); foreach (var finding in findingsReports) { var factsArray = new JArray( - new JObject(new JProperty("name", "Anlegg"), new JProperty("value", finding.PlantName)), - new JObject(new JProperty("name", "Område"), new JProperty("value", finding.InspectionAreaName)), - new JObject(new JProperty("name", "Tag Number"), new JProperty("value", finding.TagId)), - new JObject(new JProperty("name", "Beskrivelse"), new JProperty("value", finding.FindingDescription)), - new JObject(new JProperty("name", "Tidspunkt"), new JProperty("value", finding.Timestamp.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture))) + new JObject( + new JProperty("name", "Anlegg"), + new JProperty("value", finding.PlantName) + ), + new JObject( + new JProperty("name", "Område"), + new JProperty("value", finding.InspectionAreaName) + ), + new JObject( + new JProperty("name", "Tag Number"), + new JProperty("value", finding.TagId) + ), + new JObject( + new JProperty("name", "Beskrivelse"), + new JProperty("value", finding.FindingDescription) + ), + new JObject( + new JProperty("name", "Tidspunkt"), + new JProperty( + "value", + finding.Timestamp.ToString( + "yyyy-MM-dd HH:mm:ss", + CultureInfo.InvariantCulture + ) + ) + ) ); var findingObj = new JObject( new JProperty("activityTitle", $"Finding ID: \"{finding.TagId}\""), - new JProperty("facts", factsArray)); + new JProperty("facts", factsArray) + ); findingsJsonArray.Add(findingObj); } var sections = new JArray( new JObject( - new JProperty("activityTitle", $"Inspection report for \"{findingsReports[0].PlantName}\""), - new JProperty("activitySubtitle", $"Generated on: {DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)}"), - new JProperty("facts", new JArray( - new JObject( - new JProperty("name", "Number of findings:"), - new JProperty("value", numberOfFindings))))), + new JProperty( + "activityTitle", + $"Inspection report for \"{findingsReports[0].PlantName}\"" + ), + new JProperty( + "activitySubtitle", + $"Generated on: {DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)}" + ), + new JProperty( + "facts", + new JArray( + new JObject( + new JProperty("name", "Number of findings:"), + new JProperty("value", numberOfFindings) + ) + ) + ) + ), new JObject( - new JProperty("activityTitle", "The following inspection findings were identified:"))); + new JProperty( + "activityTitle", + "The following inspection findings were identified:" + ) + ) + ); foreach (var findingObj in findingsJsonArray) sections.Add(findingObj); @@ -122,14 +204,17 @@ public static string GenerateAdaptiveCard(string title, int numberOfFindings, Li new JProperty("summary", "Inspection Findings Report"), new JProperty("themeColor", "0078D7"), new JProperty("title", $"Inspection Findings: \"{title}\""), - new JProperty("sections", sections)); + new JProperty("sections", sections) + ); return adaptiveCardObj.ToString(Formatting.Indented); } public static string GetWebhookURL(IConfiguration configuration, string secretName) { - string? keyVaultUri = configuration.GetSection("KeyVault")["VaultUri"] ?? throw new KeyNotFoundException("No key vault in config"); + string? keyVaultUri = + configuration.GetSection("KeyVault")["VaultUri"] + ?? throw new KeyNotFoundException("No key vault in config"); var keyVault = new SecretClient( new Uri(keyVaultUri), @@ -138,15 +223,19 @@ public static string GetWebhookURL(IConfiguration configuration, string secretNa ) ); - string webhookURL = keyVault - .GetSecret(secretName) - .Value.Value; + string webhookURL = keyVault.GetSecret(secretName).Value.Value; return webhookURL; } } - public class Finding(string tagId, string plantName, string inspectionAreaName, string findingDescription, DateTime timestamp) + public class Finding( + string tagId, + string plantName, + string inspectionAreaName, + string findingDescription, + DateTime timestamp + ) { public string TagId { get; set; } = tagId; public string PlantName { get; set; } = plantName; diff --git a/backend/api/EventHandlers/IsarConnectionEventHandler.cs b/backend/api/EventHandlers/IsarConnectionEventHandler.cs index 66b3cef2d..e4a852b78 100644 --- a/backend/api/EventHandlers/IsarConnectionEventHandler.cs +++ b/backend/api/EventHandlers/IsarConnectionEventHandler.cs @@ -7,6 +7,7 @@ using Api.Services.Events; using Api.Utilities; using Timer = System.Timers.Timer; + namespace Api.EventHandlers { /// @@ -14,7 +15,6 @@ namespace Api.EventHandlers /// public class IsarConnectionEventHandler : EventHandlerBase { - private readonly int _isarConnectionTimeout; private readonly ConcurrentDictionary _isarConnectionTimers = new(); @@ -44,7 +44,9 @@ IConfiguration config _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private IMissionSchedulingService MissionSchedulingService => - _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); public override void Subscribe() { @@ -76,7 +78,10 @@ private async void OnIsarRobotHeartbeat(object? sender, MqttReceivedArgs mqttArg return; } - if (!_isarConnectionTimers.ContainsKey(robot.IsarId)) { AddTimerForRobot(isarRobotHeartbeat, robot); } + if (!_isarConnectionTimers.ContainsKey(robot.IsarId)) + { + AddTimerForRobot(isarRobotHeartbeat, robot); + } _logger.LogDebug( "Reset connection timer for ISAR '{IsarId}' ('{RobotName}')", @@ -86,7 +91,10 @@ private async void OnIsarRobotHeartbeat(object? sender, MqttReceivedArgs mqttArg _isarConnectionTimers[robot.IsarId].Reset(); - if (robot.IsarConnected) { return; } + if (robot.IsarConnected) + { + return; + } try { await RobotService.UpdateRobotIsarConnected(robot.Id, true); @@ -104,7 +112,10 @@ private async void OnIsarRobotHeartbeat(object? sender, MqttReceivedArgs mqttArg // If the robot became available while the connection was not active, then this will not be triggered // It will however be triggered if the robot lost connection while restarting or while idle - if (robot.Status == RobotStatus.Available) MissionSchedulingService.TriggerRobotAvailable(new RobotAvailableEventArgs(robot.Id)); + if (robot.Status == RobotStatus.Available) + MissionSchedulingService.TriggerRobotAvailable( + new RobotAvailableEventArgs(robot.Id) + ); } private void AddTimerForRobot(IsarRobotHeartbeatMessage isarRobotHeartbeat, Robot robot) @@ -113,17 +124,31 @@ private void AddTimerForRobot(IsarRobotHeartbeatMessage isarRobotHeartbeat, Robo timer.Elapsed += (_, _) => OnTimeoutEvent(isarRobotHeartbeat); timer.Start(); - if (_isarConnectionTimers.TryAdd(robot.IsarId, timer)) { _logger.LogInformation("Added new timer for ISAR '{IsarId}' ('{RobotName}')", robot.IsarId, robot.Name); } + if (_isarConnectionTimers.TryAdd(robot.IsarId, timer)) + { + _logger.LogInformation( + "Added new timer for ISAR '{IsarId}' ('{RobotName}')", + robot.IsarId, + robot.Name + ); + } else { - _logger.LogWarning("Failed to add new timer for ISAR '{IsarId}' ('{RobotName})'", robot.IsarId, robot.Name); + _logger.LogWarning( + "Failed to add new timer for ISAR '{IsarId}' ('{RobotName})'", + robot.IsarId, + robot.Name + ); timer.Close(); } } private async void OnTimeoutEvent(IsarRobotHeartbeatMessage robotHeartbeatMessage) { - var robot = await RobotService.ReadByIsarId(robotHeartbeatMessage.IsarId, readOnly: true); + var robot = await RobotService.ReadByIsarId( + robotHeartbeatMessage.IsarId, + readOnly: true + ); if (robot is null) { _logger.LogError( @@ -141,7 +166,10 @@ private async void OnTimeoutEvent(IsarRobotHeartbeatMessage robotHeartbeatMessag if (robot.CurrentMissionId != null) { - var missionRun = await MissionRunService.ReadById(robot.CurrentMissionId, readOnly: true); + var missionRun = await MissionRunService.ReadById( + robot.CurrentMissionId, + readOnly: true + ); if (missionRun != null) { _logger.LogError( @@ -149,7 +177,10 @@ private async void OnTimeoutEvent(IsarRobotHeartbeatMessage robotHeartbeatMessag missionRun.Id, missionRun.Name ); - await MissionRunService.SetMissionRunToFailed(missionRun.Id, "Lost connection to ISAR during mission"); + await MissionRunService.SetMissionRunToFailed( + missionRun.Id, + "Lost connection to ISAR during mission" + ); } } @@ -158,13 +189,23 @@ private async void OnTimeoutEvent(IsarRobotHeartbeatMessage robotHeartbeatMessag await RobotService.UpdateRobotIsarConnected(robot.Id, false); await RobotService.UpdateCurrentMissionId(robot.Id, null); } - catch (RobotNotFoundException) { return; } + catch (RobotNotFoundException) + { + return; + } } - if (!_isarConnectionTimers.TryGetValue(robotHeartbeatMessage.IsarId, out var timer)) { return; } + if (!_isarConnectionTimers.TryGetValue(robotHeartbeatMessage.IsarId, out var timer)) + { + return; + } timer.Close(); _isarConnectionTimers.Remove(robotHeartbeatMessage.IsarId, out _); - _logger.LogError("Removed timer for ISAR instance {RobotName} with ID '{Id}'", robotHeartbeatMessage.RobotName, robotHeartbeatMessage.IsarId); + _logger.LogError( + "Removed timer for ISAR instance {RobotName} with ID '{Id}'", + robotHeartbeatMessage.RobotName, + robotHeartbeatMessage.IsarId + ); } } } diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 2dfecd427..a9fd82a0d 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -2,9 +2,9 @@ using Api.Services; using Api.Services.Events; using Api.Utilities; + namespace Api.EventHandlers { - public class MissionEventHandler : EventHandlerBase { private readonly ILogger _logger; @@ -24,16 +24,23 @@ IServiceScopeFactory scopeFactory Subscribe(); } - private IMissionRunService MissionService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IRobotService RobotService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IMissionRunService MissionService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IMissionSchedulingService MissionScheduling => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IRobotService RobotService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private ISignalRService SignalRService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IMissionSchedulingService MissionScheduling => + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); - private IReturnToHomeService ReturnToHomeService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private ISignalRService SignalRService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IReturnToHomeService ReturnToHomeService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); public override void Subscribe() { @@ -58,78 +65,153 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArgs e) { - _logger.LogInformation("Triggered MissionRunCreated event for mission run ID: {MissionRunId}", e.MissionRunId); + _logger.LogInformation( + "Triggered MissionRunCreated event for mission run ID: {MissionRunId}", + e.MissionRunId + ); var missionRun = await MissionService.ReadById(e.MissionRunId, readOnly: true); if (missionRun == null) { - _logger.LogError("Mission run with ID: {MissionRunId} was not found in the database", e.MissionRunId); + _logger.LogError( + "Mission run with ID: {MissionRunId} was not found in the database", + e.MissionRunId + ); return; } _startMissionSemaphore.WaitOne(); - if (missionRun.MissionRunType != MissionRunType.ReturnHome && await ReturnToHomeService.GetActiveReturnToHomeMissionRun(missionRun.Robot.Id, readOnly: true) != null) + if ( + missionRun.MissionRunType != MissionRunType.ReturnHome + && await ReturnToHomeService.GetActiveReturnToHomeMissionRun( + missionRun.Robot.Id, + readOnly: true + ) != null + ) { await MissionScheduling.AbortActiveReturnToHomeMission(missionRun.Robot.Id); } - try { await MissionScheduling.StartNextMissionRunIfSystemIsAvailable(missionRun.Robot); } - catch (MissionRunNotFoundException) { return; } - finally { _startMissionSemaphore.Release(); } + try + { + await MissionScheduling.StartNextMissionRunIfSystemIsAvailable(missionRun.Robot); + } + catch (MissionRunNotFoundException) + { + return; + } + finally + { + _startMissionSemaphore.Release(); + } } private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) { - _logger.LogInformation("Triggered RobotAvailable event for robot ID: {RobotId}", e.RobotId); + _logger.LogInformation( + "Triggered RobotAvailable event for robot ID: {RobotId}", + e.RobotId + ); var robot = await RobotService.ReadById(e.RobotId, readOnly: true); if (robot == null) { - _logger.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId); + _logger.LogError( + "Robot with ID: {RobotId} was not found in the database", + e.RobotId + ); return; } _startMissionSemaphore.WaitOne(); - try { await MissionScheduling.StartNextMissionRunIfSystemIsAvailable(robot); } - catch (MissionRunNotFoundException) { return; } - finally { _startMissionSemaphore.Release(); } + try + { + await MissionScheduling.StartNextMissionRunIfSystemIsAvailable(robot); + } + catch (MissionRunNotFoundException) + { + return; + } + finally + { + _startMissionSemaphore.Release(); + } } private async void OnSendRobotToDockTriggered(object? sender, RobotEmergencyEventArgs e) { - _logger.LogInformation("Triggered EmergencyButtonPressed event for robot ID: {RobotId}", e.RobotId); + _logger.LogInformation( + "Triggered EmergencyButtonPressed event for robot ID: {RobotId}", + e.RobotId + ); var robot = await RobotService.ReadById(e.RobotId, readOnly: true); if (robot == null) { - _logger.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId); + _logger.LogError( + "Robot with ID: {RobotId} was not found in the database", + e.RobotId + ); return; } - try { await MissionScheduling.FreezeMissionRunQueueForRobot(e.RobotId); } - catch (RobotNotFoundException) { return; } + try + { + await MissionScheduling.FreezeMissionRunQueueForRobot(e.RobotId); + } + catch (RobotNotFoundException) + { + return; + } if (robot.FlotillaStatus == e.RobotFlotillaStatus) { - _logger.LogInformation("Did not send robot to Dock since robot {RobotId} was already in the correct state", e.RobotId); + _logger.LogInformation( + "Did not send robot to Dock since robot {RobotId} was already in the correct state", + e.RobotId + ); return; } - try { await RobotService.UpdateFlotillaStatus(e.RobotId, e.RobotFlotillaStatus); } + try + { + await RobotService.UpdateFlotillaStatus(e.RobotId, e.RobotFlotillaStatus); + } catch (Exception ex) { - _logger.LogError("Was not able to update Robot Flotilla status for robot {RobotId}, {ErrorMessage}", e.RobotId, ex.Message); + _logger.LogError( + "Was not able to update Robot Flotilla status for robot {RobotId}, {ErrorMessage}", + e.RobotId, + ex.Message + ); return; } - try { await MissionScheduling.ScheduleMissionToDriveToDockPosition(e.RobotId); } + try + { + await MissionScheduling.ScheduleMissionToDriveToDockPosition(e.RobotId); + } catch (DockException ex) { - _logger.LogError(ex, "Failed to schedule return to dock mission on robot {RobotName} because: {ErrorMessage}", robot.Name, ex.Message); - SignalRService.ReportDockFailureToSignalR(robot, $"Failed to send {robot.Name} to a dock"); + _logger.LogError( + ex, + "Failed to schedule return to dock mission on robot {RobotName} because: {ErrorMessage}", + robot.Name, + ex.Message + ); + SignalRService.ReportDockFailureToSignalR( + robot, + $"Failed to send {robot.Name} to a dock" + ); } - try { await MissionScheduling.StopCurrentMissionRun(e.RobotId); } - catch (RobotNotFoundException) { return; } + try + { + await MissionScheduling.StopCurrentMissionRun(e.RobotId); + } + catch (RobotNotFoundException) + { + return; + } catch (MissionRunNotFoundException) { /* Allow robot to return to dock if there is no ongoing mission */ @@ -139,57 +221,112 @@ private async void OnSendRobotToDockTriggered(object? sender, RobotEmergencyEven // We want to continue driving to the dock if the isar state is idle if (ex.IsarStatusCode != StatusCodes.Status409Conflict) { - _logger.LogError(ex, "Failed to stop the current mission on robot {RobotName} because: {ErrorMessage}", robot.Name, ex.Message); - SignalRService.ReportDockFailureToSignalR(robot, $"Failed to stop current mission for robot {robot.Name}"); + _logger.LogError( + ex, + "Failed to stop the current mission on robot {RobotName} because: {ErrorMessage}", + robot.Name, + ex.Message + ); + SignalRService.ReportDockFailureToSignalR( + robot, + $"Failed to stop current mission for robot {robot.Name}" + ); return; } } catch (Exception ex) { - const string Message = "Error in ISAR while stopping current mission, cannot drive to docking station."; - SignalRService.ReportDockFailureToSignalR(robot, $"Robot {robot.Name} failed to drive to docking station."); + const string Message = + "Error in ISAR while stopping current mission, cannot drive to docking station."; + SignalRService.ReportDockFailureToSignalR( + robot, + $"Robot {robot.Name} failed to drive to docking station." + ); _logger.LogError(ex, "{Message}", Message); return; } _startMissionSemaphore.WaitOne(); - try { await MissionScheduling.StartNextMissionRunIfSystemIsAvailable(robot); } - catch (MissionRunNotFoundException) { return; } - finally { _startMissionSemaphore.Release(); } + try + { + await MissionScheduling.StartNextMissionRunIfSystemIsAvailable(robot); + } + catch (MissionRunNotFoundException) + { + return; + } + finally + { + _startMissionSemaphore.Release(); + } } - private async void OnReleaseRobotFromDockTriggered(object? sender, RobotEmergencyEventArgs e) + private async void OnReleaseRobotFromDockTriggered( + object? sender, + RobotEmergencyEventArgs e + ) { - _logger.LogInformation("Triggered EmergencyButtonPressed event for robot ID: {RobotId}", e.RobotId); + _logger.LogInformation( + "Triggered EmergencyButtonPressed event for robot ID: {RobotId}", + e.RobotId + ); var robot = await RobotService.ReadById(e.RobotId, readOnly: true); if (robot == null) { - _logger.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId); + _logger.LogError( + "Robot with ID: {RobotId} was not found in the database", + e.RobotId + ); return; } if (robot.FlotillaStatus == e.RobotFlotillaStatus) { - _logger.LogInformation("Did not release robot from Dock since robot {RobotId} was already in the correct state", e.RobotId); + _logger.LogInformation( + "Did not release robot from Dock since robot {RobotId} was already in the correct state", + e.RobotId + ); return; } - try { await MissionScheduling.UnfreezeMissionRunQueueForRobot(robot.Id); } - catch (RobotNotFoundException) { return; } + try + { + await MissionScheduling.UnfreezeMissionRunQueueForRobot(robot.Id); + } + catch (RobotNotFoundException) + { + return; + } robot.MissionQueueFrozen = false; - try { await RobotService.UpdateFlotillaStatus(e.RobotId, e.RobotFlotillaStatus); } + try + { + await RobotService.UpdateFlotillaStatus(e.RobotId, e.RobotFlotillaStatus); + } catch (Exception ex) { - _logger.LogError("Was not able to update Robot Flotilla status for robot {RobotId}, {ErrorMessage}", e.RobotId, ex.Message); + _logger.LogError( + "Was not able to update Robot Flotilla status for robot {RobotId}, {ErrorMessage}", + e.RobotId, + ex.Message + ); return; } _startMissionSemaphore.WaitOne(); - try { await MissionScheduling.StartNextMissionRunIfSystemIsAvailable(robot); } - catch (MissionRunNotFoundException) { return; } - finally { _startMissionSemaphore.Release(); } + try + { + await MissionScheduling.StartNextMissionRunIfSystemIsAvailable(robot); + } + catch (MissionRunNotFoundException) + { + return; + } + finally + { + _startMissionSemaphore.Release(); + } } } } diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index a39118155..68ba8a14b 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -19,7 +19,6 @@ public class MqttEventHandler : EventHandlerBase { private readonly ILogger _logger; - private readonly IServiceScopeFactory _scopeFactory; private readonly Semaphore _updateRobotSemaphore = new(1, 1); @@ -33,20 +32,46 @@ public MqttEventHandler(ILogger logger, IServiceScopeFactory s Subscribe(); } - private IBatteryTimeseriesService BatteryTimeseriesService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IInspectionService InspectionService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IInstallationService InstallationService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private ILastMissionRunService LastMissionRunService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IMissionRunService MissionRunService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IMissionSchedulingService MissionScheduling => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IMissionTaskService MissionTaskService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IPressureTimeseriesService PressureTimeseriesService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IRobotService RobotService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IPoseTimeseriesService PoseTimeseriesService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private ISignalRService SignalRService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private ITaskDurationService TaskDurationService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private ITeamsMessageService TeamsMessageService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IEmergencyActionService EmergencyActionService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IBatteryTimeseriesService BatteryTimeseriesService => + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); + private IInspectionService InspectionService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IInstallationService InstallationService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private ILastMissionRunService LastMissionRunService => + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); + private IMissionRunService MissionRunService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IMissionSchedulingService MissionScheduling => + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); + private IMissionTaskService MissionTaskService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IPressureTimeseriesService PressureTimeseriesService => + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); + private IRobotService RobotService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IPoseTimeseriesService PoseTimeseriesService => + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); + private ISignalRService SignalRService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private ITaskDurationService TaskDurationService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private ITeamsMessageService TeamsMessageService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IEmergencyActionService EmergencyActionService => + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); public override void Subscribe() { @@ -72,8 +97,10 @@ public override void Unsubscribe() MqttService.MqttIsarCloudHealthReceived -= OnIsarCloudHealthUpdate; } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await stoppingToken; } - + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await stoppingToken; + } private async void OnIsarStatus(object? sender, MqttReceivedArgs mqttArgs) { @@ -83,13 +110,25 @@ private async void OnIsarStatus(object? sender, MqttReceivedArgs mqttArgs) if (robot == null) { - _logger.LogInformation("Received message from unknown ISAR instance {Id} with robot name {Name}", isarStatus.IsarId, isarStatus.RobotName); + _logger.LogInformation( + "Received message from unknown ISAR instance {Id} with robot name {Name}", + isarStatus.IsarId, + isarStatus.RobotName + ); return; } - if (robot.Status == isarStatus.Status) { return; } + if (robot.Status == isarStatus.Status) + { + return; + } - _logger.LogInformation("OnIsarStatus: Robot {robotName} has status {robotStatus} and current inspection area {areaName}", robot.Name, robot.Status, robot.CurrentInspectionArea?.Name); + _logger.LogInformation( + "OnIsarStatus: Robot {robotName} has status {robotStatus} and current inspection area {areaName}", + robot.Name, + robot.Status, + robot.CurrentInspectionArea?.Name + ); _updateRobotSemaphore.WaitOne(); _logger.LogDebug("Semaphore acquired for updating robot status"); @@ -100,10 +139,18 @@ private async void OnIsarStatus(object? sender, MqttReceivedArgs mqttArgs) _updateRobotSemaphore.Release(); _logger.LogDebug("Semaphore released after updating robot status"); - _logger.LogInformation("Updated status for robot {Name} to {Status}", robot.Name, robot.Status); - + _logger.LogInformation( + "Updated status for robot {Name} to {Status}", + robot.Name, + robot.Status + ); - _logger.LogInformation("OnIsarStatus: Robot {robotName} has status {robotStatus} and current inspection area {areaName}", robot.Name, robot.Status, robot.CurrentInspectionArea?.Name); + _logger.LogInformation( + "OnIsarStatus: Robot {robotName} has status {robotStatus} and current inspection area {areaName}", + robot.Name, + robot.Status, + robot.CurrentInspectionArea?.Name + ); if (isarStatus.Status == RobotStatus.Available) { @@ -116,7 +163,10 @@ private async void OnIsarStatus(object? sender, MqttReceivedArgs mqttArgs) } catch (RobotNotFoundException) { - _logger.LogError("Robot {robotName} not found when updating current mission id to null", robot.Name); + _logger.LogError( + "Robot {robotName} not found when updating current mission id to null", + robot.Name + ); return; } finally @@ -128,11 +178,16 @@ private async void OnIsarStatus(object? sender, MqttReceivedArgs mqttArgs) } } - private async void CreateRobot(IsarRobotInfoMessage isarRobotInfo, Installation installation) + private async void CreateRobot( + IsarRobotInfoMessage isarRobotInfo, + Installation installation + ) { _logger.LogInformation( "Received message from new ISAR instance '{Id}' with robot name '{Name}'. Adding new robot to database", - isarRobotInfo.IsarId, isarRobotInfo.RobotName); + isarRobotInfo.IsarId, + isarRobotInfo.RobotName + ); var robotQuery = new CreateRobotQuery { @@ -151,7 +206,11 @@ private async void CreateRobot(IsarRobotInfoMessage isarRobotInfo, Installation try { var newRobot = await RobotService.CreateFromQuery(robotQuery); - _logger.LogInformation("Added robot '{RobotName}' with ISAR id '{IsarId}' to database", newRobot.Name, newRobot.IsarId); + _logger.LogInformation( + "Added robot '{RobotName}' with ISAR id '{IsarId}' to database", + newRobot.Name, + newRobot.IsarId + ); } catch (DbUpdateException) { @@ -164,12 +223,17 @@ private async void OnIsarRobotInfo(object? sender, MqttReceivedArgs mqttArgs) { var isarRobotInfo = (IsarRobotInfoMessage)mqttArgs.Message; - var installation = await InstallationService.ReadByInstallationCode(isarRobotInfo.CurrentInstallation, readOnly: true); + var installation = await InstallationService.ReadByInstallationCode( + isarRobotInfo.CurrentInstallation, + readOnly: true + ); if (installation is null) { _logger.LogError( - new InstallationNotFoundException($"No installation with code {isarRobotInfo.CurrentInstallation} found"), + new InstallationNotFoundException( + $"No installation with code {isarRobotInfo.CurrentInstallation} found" + ), "Could not create new robot due to missing installation" ); return; @@ -192,57 +256,111 @@ private async void OnIsarRobotInfo(object? sender, MqttReceivedArgs mqttArgs) List updatedFields = []; - if (isarRobotInfo.Host is not null) UpdateHostIfChanged(isarRobotInfo.Host, ref robot, ref updatedFields); + if (isarRobotInfo.Host is not null) + UpdateHostIfChanged(isarRobotInfo.Host, ref robot, ref updatedFields); UpdatePortIfChanged(isarRobotInfo.Port, ref robot, ref updatedFields); - if (isarRobotInfo.CurrentInstallation is not null) UpdateCurrentInstallationIfChanged(installation, ref robot, ref updatedFields); - if (isarRobotInfo.Capabilities is not null) UpdateRobotCapabilitiesIfChanged(isarRobotInfo.Capabilities, ref robot, ref updatedFields); - if (updatedFields.Count < 1) return; + if (isarRobotInfo.CurrentInstallation is not null) + UpdateCurrentInstallationIfChanged( + installation, + ref robot, + ref updatedFields + ); + if (isarRobotInfo.Capabilities is not null) + UpdateRobotCapabilitiesIfChanged( + isarRobotInfo.Capabilities, + ref robot, + ref updatedFields + ); + if (updatedFields.Count < 1) + return; await RobotService.Update(robot); - _logger.LogInformation("Updated robot '{Id}' ('{RobotName}') in database: {Updates}", robot.Id, robot.Name, updatedFields); + _logger.LogInformation( + "Updated robot '{Id}' ('{RobotName}') in database: {Updates}", + robot.Id, + robot.Name, + updatedFields + ); } finally { _updateRobotSemaphore.Release(); _logger.LogDebug("Semaphore released after updating robot"); } - } - catch (DbUpdateException e) { _logger.LogError(e, "Could not add robot to db"); } - catch (Exception e) { _logger.LogError(e, "Could not update robot in db"); } + catch (DbUpdateException e) + { + _logger.LogError(e, "Could not add robot to db"); + } + catch (Exception e) + { + _logger.LogError(e, "Could not update robot in db"); + } } - private static void UpdateHostIfChanged(string host, ref Robot robot, ref List updatedFields) + private static void UpdateHostIfChanged( + string host, + ref Robot robot, + ref List updatedFields + ) { - if (host.Equals(robot.Host, StringComparison.Ordinal)) return; + if (host.Equals(robot.Host, StringComparison.Ordinal)) + return; updatedFields.Add($"\nHost ({robot.Host} -> {host})\n"); robot.Host = host; } - private static void UpdatePortIfChanged(int port, ref Robot robot, ref List updatedFields) + private static void UpdatePortIfChanged( + int port, + ref Robot robot, + ref List updatedFields + ) { - if (port.Equals(robot.Port)) return; + if (port.Equals(robot.Port)) + return; updatedFields.Add($"\nPort ({robot.Port} -> {port})\n"); robot.Port = port; } - private static void UpdateCurrentInstallationIfChanged(Installation newCurrentInstallation, ref Robot robot, ref List updatedFields) + private static void UpdateCurrentInstallationIfChanged( + Installation newCurrentInstallation, + ref Robot robot, + ref List updatedFields + ) { - if (newCurrentInstallation.InstallationCode.Equals(robot.CurrentInstallation?.InstallationCode, StringComparison.Ordinal)) return; + if ( + newCurrentInstallation.InstallationCode.Equals( + robot.CurrentInstallation?.InstallationCode, + StringComparison.Ordinal + ) + ) + return; - updatedFields.Add($"\nCurrentInstallation ({robot.CurrentInstallation} -> {newCurrentInstallation})\n"); + updatedFields.Add( + $"\nCurrentInstallation ({robot.CurrentInstallation} -> {newCurrentInstallation})\n" + ); robot.CurrentInstallation = newCurrentInstallation; } - public static void UpdateRobotCapabilitiesIfChanged(IList newRobotCapabilities, ref Robot robot, ref List updatedFields) + public static void UpdateRobotCapabilitiesIfChanged( + IList newRobotCapabilities, + ref Robot robot, + ref List updatedFields + ) { - if (robot.RobotCapabilities != null && Enumerable.SequenceEqual(newRobotCapabilities, robot.RobotCapabilities)) return; + if ( + robot.RobotCapabilities != null + && Enumerable.SequenceEqual(newRobotCapabilities, robot.RobotCapabilities) + ) + return; - updatedFields.Add($"\nRobotCapabilities ({robot.RobotCapabilities} -> {newRobotCapabilities})\n"); + updatedFields.Add( + $"\nRobotCapabilities ({robot.RobotCapabilities} -> {newRobotCapabilities})\n" + ); robot.RobotCapabilities = newRobotCapabilities; } @@ -251,54 +369,110 @@ private async void OnIsarMissionUpdate(object? sender, MqttReceivedArgs mqttArgs var isarMission = (IsarMissionMessage)mqttArgs.Message; MissionStatus status; - try { status = MissionRun.GetMissionStatusFromString(isarMission.Status); } + try + { + status = MissionRun.GetMissionStatusFromString(isarMission.Status); + } catch (ArgumentException e) { - _logger.LogError(e, "Failed to parse mission status from MQTT message. Mission with ISARMissionId '{IsarMissionId}' was not updated", isarMission.MissionId); + _logger.LogError( + e, + "Failed to parse mission status from MQTT message. Mission with ISARMissionId '{IsarMissionId}' was not updated", + isarMission.MissionId + ); return; } - var flotillaMissionRun = await MissionRunService.ReadByIsarMissionId(isarMission.MissionId, readOnly: true); + var flotillaMissionRun = await MissionRunService.ReadByIsarMissionId( + isarMission.MissionId, + readOnly: true + ); if (flotillaMissionRun is null) { - string errorMessage = $"Mission with isar mission Id {isarMission.IsarId} was not found"; + string errorMessage = + $"Mission with isar mission Id {isarMission.IsarId} was not found"; _logger.LogError("{Message}", errorMessage); return; } - if (flotillaMissionRun.Status == status) { return; } - if (flotillaMissionRun.Status == MissionStatus.Aborted && status == MissionStatus.Cancelled) { status = MissionStatus.Aborted; } + if (flotillaMissionRun.Status == status) + { + return; + } + if ( + flotillaMissionRun.Status == MissionStatus.Aborted + && status == MissionStatus.Cancelled + ) + { + status = MissionStatus.Aborted; + } MissionRun updatedFlotillaMissionRun; - try { updatedFlotillaMissionRun = await MissionRunService.UpdateMissionRunStatusByIsarMissionId(isarMission.MissionId, status); } - catch (MissionRunNotFoundException) { return; } + try + { + updatedFlotillaMissionRun = + await MissionRunService.UpdateMissionRunStatusByIsarMissionId( + isarMission.MissionId, + status + ); + } + catch (MissionRunNotFoundException) + { + return; + } _logger.LogInformation( "Mission '{Id}' (ISARMissionID='{IsarMissionId}') status updated to '{Status}' for robot '{RobotName}' with ISAR id '{IsarId}'", - updatedFlotillaMissionRun.Id, isarMission.MissionId, isarMission.Status, isarMission.RobotName, isarMission.IsarId + updatedFlotillaMissionRun.Id, + isarMission.MissionId, + isarMission.Status, + isarMission.RobotName, + isarMission.IsarId ); - if (!updatedFlotillaMissionRun.IsCompleted) return; + if (!updatedFlotillaMissionRun.IsCompleted) + return; var robot = await RobotService.ReadByIsarId(isarMission.IsarId, readOnly: true); if (robot is null) { - _logger.LogError("Could not find robot '{RobotName}' with ISAR id '{IsarId}'", isarMission.RobotName, isarMission.IsarId); + _logger.LogError( + "Could not find robot '{RobotName}' with ISAR id '{IsarId}'", + isarMission.RobotName, + isarMission.IsarId + ); return; } - _logger.LogInformation("Robot '{Id}' ('{Name}') - completed mission run {MissionRunId}", robot.IsarId, robot.Name, updatedFlotillaMissionRun.Id); + _logger.LogInformation( + "Robot '{Id}' ('{Name}') - completed mission run {MissionRunId}", + robot.IsarId, + robot.Name, + updatedFlotillaMissionRun.Id + ); if (updatedFlotillaMissionRun.MissionId == null) { - _logger.LogInformation("Mission run {missionRunId} does not have a mission definition assosiated with it", updatedFlotillaMissionRun.Id); + _logger.LogInformation( + "Mission run {missionRunId} does not have a mission definition assosiated with it", + updatedFlotillaMissionRun.Id + ); return; } - try { await LastMissionRunService.SetLastMissionRun(updatedFlotillaMissionRun.Id, updatedFlotillaMissionRun.MissionId); } + try + { + await LastMissionRunService.SetLastMissionRun( + updatedFlotillaMissionRun.Id, + updatedFlotillaMissionRun.MissionId + ); + } catch (MissionNotFoundException) { - _logger.LogError("Mission not found when setting last mission run for mission definition {missionId}", updatedFlotillaMissionRun.MissionId); + _logger.LogError( + "Mission not found when setting last mission run for mission definition {missionId}", + updatedFlotillaMissionRun.MissionId + ); return; } @@ -310,32 +484,63 @@ private async void OnIsarTaskUpdate(object? sender, MqttReceivedArgs mqttArgs) var task = (IsarTaskMessage)mqttArgs.Message; IsarTaskStatus status; - try { status = IsarTask.StatusFromString(task.Status); } + try + { + status = IsarTask.StatusFromString(task.Status); + } catch (ArgumentException e) { - _logger.LogError(e, "Failed to parse mission status from MQTT message. Mission '{Id}' was not updated", task.MissionId); + _logger.LogError( + e, + "Failed to parse mission status from MQTT message. Mission '{Id}' was not updated", + task.MissionId + ); return; } - try { await MissionTaskService.UpdateMissionTaskStatus(task.TaskId, status); } - catch (MissionTaskNotFoundException) { return; } + try + { + await MissionTaskService.UpdateMissionTaskStatus(task.TaskId, status); + } + catch (MissionTaskNotFoundException) + { + return; + } if (task.GetMissionTaskTypeFromIsarTask(task.TaskType) == MissionTaskType.Inspection) { - try { await InspectionService.UpdateInspectionStatus(task.TaskId, status); } - catch (InspectionNotFoundException) { return; } + try + { + await InspectionService.UpdateInspectionStatus(task.TaskId, status); + } + catch (InspectionNotFoundException) + { + return; + } } - var missionRun = await MissionRunService.ReadByIsarMissionId(task.MissionId, readOnly: true); + var missionRun = await MissionRunService.ReadByIsarMissionId( + task.MissionId, + readOnly: true + ); if (missionRun is null) { _logger.LogWarning("Mission run with ID {Id} was not found", task.MissionId); } - _ = SignalRService.SendMessageAsync("Mission run updated", missionRun?.InspectionArea?.Installation, missionRun != null ? new MissionRunResponse(missionRun) : null); + _ = SignalRService.SendMessageAsync( + "Mission run updated", + missionRun?.InspectionArea?.Installation, + missionRun != null ? new MissionRunResponse(missionRun) : null + ); _logger.LogInformation( - "Task '{Id}' updated to '{Status}' for robot '{RobotName}' with ISAR id '{IsarId}'", task.TaskId, task.Status, task.RobotName, task.IsarId); + "Task '{Id}' updated to '{Status}' for robot '{RobotName}' with ISAR id '{IsarId}'", + task.TaskId, + task.Status, + task.RobotName, + task.IsarId + ); } private async void OnIsarBatteryUpdate(object? sender, MqttReceivedArgs mqttArgs) @@ -345,7 +550,10 @@ private async void OnIsarBatteryUpdate(object? sender, MqttReceivedArgs mqttArgs _updateRobotSemaphore.WaitOne(); _logger.LogDebug("Semaphore acquired for updating battery"); - var robot = await BatteryTimeseriesService.AddBatteryEntry(batteryStatus.BatteryLevel, batteryStatus.IsarId); + var robot = await BatteryTimeseriesService.AddBatteryEntry( + batteryStatus.BatteryLevel, + batteryStatus.IsarId + ); if (robot != null && robot.BatteryState != batteryStatus.BatteryState) { await RobotService.UpdateRobotBatteryState(robot.Id, batteryStatus.BatteryState); @@ -354,18 +562,32 @@ private async void OnIsarBatteryUpdate(object? sender, MqttReceivedArgs mqttArgs _updateRobotSemaphore.Release(); _logger.LogDebug("Semaphore released after updating battery"); - if (robot == null) return; + if (robot == null) + return; robot.BatteryLevel = batteryStatus.BatteryLevel; if (robot.FlotillaStatus == RobotFlotillaStatus.Normal && robot.IsRobotBatteryTooLow()) { - _logger.LogInformation("Sending robot '{RobotName}' to its dock as its battery level is too low.", robot.Name); - EmergencyActionService.SendRobotToDock(new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Recharging)); + _logger.LogInformation( + "Sending robot '{RobotName}' to its dock as its battery level is too low.", + robot.Name + ); + EmergencyActionService.SendRobotToDock( + new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Recharging) + ); } - else if (robot.FlotillaStatus == RobotFlotillaStatus.Recharging && robot.IsRobotReadyToStartMissions()) + else if ( + robot.FlotillaStatus == RobotFlotillaStatus.Recharging + && robot.IsRobotReadyToStartMissions() + ) { - _logger.LogInformation("Releasing robot '{RobotName}' from its dock as its battery and pressure levels are good enough to run missions.", robot.Name); - EmergencyActionService.ReleaseRobotFromDock(new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Normal)); + _logger.LogInformation( + "Releasing robot '{RobotName}' from its dock as its battery and pressure levels are good enough to run missions.", + robot.Name + ); + EmergencyActionService.ReleaseRobotFromDock( + new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Normal) + ); } } @@ -376,23 +598,43 @@ private async void OnIsarPressureUpdate(object? sender, MqttReceivedArgs mqttArg _updateRobotSemaphore.WaitOne(); _logger.LogDebug("Semaphore acquired for updating pressure"); - var robot = await PressureTimeseriesService.AddPressureEntry(pressureStatus.PressureLevel, pressureStatus.IsarId); + var robot = await PressureTimeseriesService.AddPressureEntry( + pressureStatus.PressureLevel, + pressureStatus.IsarId + ); _updateRobotSemaphore.Release(); _logger.LogDebug("Semaphore released after updating pressure"); - if (robot == null) return; + if (robot == null) + return; robot.PressureLevel = pressureStatus.PressureLevel; - if (robot.FlotillaStatus == RobotFlotillaStatus.Normal && (robot.IsRobotPressureTooLow() || robot.IsRobotPressureTooHigh())) + if ( + robot.FlotillaStatus == RobotFlotillaStatus.Normal + && (robot.IsRobotPressureTooLow() || robot.IsRobotPressureTooHigh()) + ) { - _logger.LogInformation("Sending robot '{RobotName}' to its dock as its pressure is too low or high.", robot.Name); - EmergencyActionService.SendRobotToDock(new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Recharging)); + _logger.LogInformation( + "Sending robot '{RobotName}' to its dock as its pressure is too low or high.", + robot.Name + ); + EmergencyActionService.SendRobotToDock( + new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Recharging) + ); } - else if (robot.FlotillaStatus == RobotFlotillaStatus.Recharging && robot.IsRobotReadyToStartMissions()) + else if ( + robot.FlotillaStatus == RobotFlotillaStatus.Recharging + && robot.IsRobotReadyToStartMissions() + ) { - _logger.LogInformation("Releasing robot '{RobotName}' from its dock as its battery and pressure levels are good enough to run missions.", robot.Name); - EmergencyActionService.ReleaseRobotFromDock(new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Normal)); + _logger.LogInformation( + "Releasing robot '{RobotName}' from its dock as its battery and pressure levels are good enough to run missions.", + robot.Name + ); + EmergencyActionService.ReleaseRobotFromDock( + new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Normal) + ); } } @@ -417,7 +659,11 @@ private async void OnIsarCloudHealthUpdate(object? sender, MqttReceivedArgs mqtt var robot = await RobotService.ReadByIsarId(cloudHealthStatus.IsarId, readOnly: true); if (robot == null) { - _logger.LogInformation("Received message from unknown ISAR instance {Id} with robot name {Name}", cloudHealthStatus.IsarId, cloudHealthStatus.RobotName); + _logger.LogInformation( + "Received message from unknown ISAR instance {Id} with robot name {Name}", + cloudHealthStatus.IsarId, + cloudHealthStatus.RobotName + ); return; } diff --git a/backend/api/EventHandlers/TeamsMessageEventHandler.cs b/backend/api/EventHandlers/TeamsMessageEventHandler.cs index adf05a3ea..7b6a5de21 100644 --- a/backend/api/EventHandlers/TeamsMessageEventHandler.cs +++ b/backend/api/EventHandlers/TeamsMessageEventHandler.cs @@ -42,9 +42,14 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private async void OnTeamsMessageReceived(object? sender, TeamsMessageEventArgs e) { - string url = InspectionFindingEventHandler.GetWebhookURL(_configuration, "TeamsSystemStatusNotification"); + string url = InspectionFindingEventHandler.GetWebhookURL( + _configuration, + "TeamsSystemStatusNotification" + ); var client = new HttpClient(); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + client.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json") + ); var content = CreateTeamsMessageCard(e.TeamsMessage); HttpResponseMessage? response; @@ -61,7 +66,9 @@ private async void OnTeamsMessageReceived(object? sender, TeamsMessageEventArgs if (!response.IsSuccessStatusCode) { string errorBody = await response.Content.ReadAsStringAsync(); - _logger.LogError($"Webhook request failed with status code {response.StatusCode}. Response body: {errorBody}"); + _logger.LogError( + $"Webhook request failed with status code {response.StatusCode}. Response body: {errorBody}" + ); } } @@ -70,8 +77,18 @@ private static StringContent CreateTeamsMessageCard(string message) string jsonMessage = new JObject( new JProperty("title", "System Status:"), new JProperty("text", message), - new JProperty("sections", new JArray(new JObject(new JProperty("activitySubtitle", $"Generated on: {DateTime.UtcNow.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture)}")))) - ).ToString(Formatting.Indented); + new JProperty( + "sections", + new JArray( + new JObject( + new JProperty( + "activitySubtitle", + $"Generated on: {DateTime.UtcNow.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture)}" + ) + ) + ) + ) + ).ToString(Formatting.Indented); var content = new StringContent(jsonMessage, Encoding.UTF8, "application/json"); diff --git a/backend/api/MQTT/MessageModels/IsarRobotHeartbeat.cs b/backend/api/MQTT/MessageModels/IsarRobotHeartbeat.cs index 7b5c32668..4260e797d 100644 --- a/backend/api/MQTT/MessageModels/IsarRobotHeartbeat.cs +++ b/backend/api/MQTT/MessageModels/IsarRobotHeartbeat.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; + namespace Api.Mqtt.MessageModels { #nullable disable diff --git a/backend/api/MQTT/MessageModels/IsarStatus.cs b/backend/api/MQTT/MessageModels/IsarStatus.cs index 2f3cc014f..ee73211ad 100644 --- a/backend/api/MQTT/MessageModels/IsarStatus.cs +++ b/backend/api/MQTT/MessageModels/IsarStatus.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Api.Database.Models; + namespace Api.Mqtt.MessageModels { #nullable disable diff --git a/backend/api/MQTT/MessageModels/IsarTask.cs b/backend/api/MQTT/MessageModels/IsarTask.cs index 10f022306..009517909 100644 --- a/backend/api/MQTT/MessageModels/IsarTask.cs +++ b/backend/api/MQTT/MessageModels/IsarTask.cs @@ -27,7 +27,6 @@ public class IsarTaskMessage : MqttMessage [JsonPropertyName("timestamp")] public DateTime Timestamp { get; set; } - public MissionTaskType GetMissionTaskTypeFromIsarTask(string isarTaskType) { return isarTaskType switch @@ -39,7 +38,7 @@ public MissionTaskType GetMissionTaskTypeFromIsarTask(string isarTaskType) "take_thermal_video" => MissionTaskType.Inspection, "return_to_home" => MissionTaskType.ReturnHome, - _ => throw new ArgumentException($"ISAR Task type '{isarTaskType}' not supported") + _ => throw new ArgumentException($"ISAR Task type '{isarTaskType}' not supported"), }; } } diff --git a/backend/api/MQTT/MqttService.cs b/backend/api/MQTT/MqttService.cs index 602694182..5b3f2b7aa 100644 --- a/backend/api/MQTT/MqttService.cs +++ b/backend/api/MQTT/MqttService.cs @@ -8,11 +8,11 @@ using MQTTnet.Client; using MQTTnet.Extensions.ManagedClient; using MQTTnet.Packets; + namespace Api.Mqtt { public class MqttService : BackgroundService { - private readonly ILogger _logger; private readonly int _maxRetryAttempts; @@ -28,13 +28,8 @@ public class MqttService : BackgroundService private readonly int _serverPort; private readonly bool _shouldFailOnMaxRetries; - private static readonly JsonSerializerOptions serializerOptions = new() - { - Converters = - { - new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) - } - }; + private static readonly JsonSerializerOptions serializerOptions = + new() { Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } }; private CancellationToken _cancellationToken; private int _reconnectAttempts; @@ -58,13 +53,12 @@ public MqttService(ILogger logger, IConfiguration config) _maxRetryAttempts = mqttConfig.GetValue("MaxRetryAttempts"); _shouldFailOnMaxRetries = mqttConfig.GetValue("ShouldFailOnMaxRetries"); - var tlsOptions = new MqttClientTlsOptions { UseTls = true, /* Currently disabled to use self-signed certificate in the internal broker communication */ //if (_notProduction) - IgnoreCertificateChainErrors = true + IgnoreCertificateChainErrors = true, }; var builder = new MqttClientOptionsBuilder() .WithTcpServer(_serverHost, _serverPort) @@ -81,6 +75,7 @@ public MqttService(ILogger logger, IConfiguration config) var topics = mqttConfig.GetSection("Topics").Get>() ?? []; SubscribeToTopics(topics); } + public static event EventHandler? MqttIsarStatusReceived; public static event EventHandler? MqttIsarRobotInfoReceived; public static event EventHandler? MqttIsarRobotHeartbeatReceived; @@ -246,21 +241,17 @@ public void SubscribeToTopics(List topics) List topicFilters = []; StringBuilder sb = new(); sb.AppendLine("Mqtt service subscribing to the following topics:"); - topics.ForEach( - topic => - { - topicFilters.Add(new MqttTopicFilter - { - Topic = topic - }); - sb.AppendLine(topic); - } - ); + topics.ForEach(topic => + { + topicFilters.Add(new MqttTopicFilter { Topic = topic }); + sb.AppendLine(topic); + }); _logger.LogInformation("{topicContent}", sb.ToString()); _mqttClient.SubscribeAsync(topicFilters).Wait(); } - private void OnIsarTopicReceived(string content) where T : MqttMessage + private void OnIsarTopicReceived(string content) + where T : MqttMessage { T? message; @@ -289,17 +280,17 @@ private void OnIsarTopicReceived(string content) where T : MqttMessage { _ when type == typeof(IsarStatusMessage) => MqttIsarStatusReceived, _ when type == typeof(IsarRobotInfoMessage) => MqttIsarRobotInfoReceived, - _ when type == typeof(IsarRobotHeartbeatMessage) => MqttIsarRobotHeartbeatReceived, + _ when type == typeof(IsarRobotHeartbeatMessage) => + MqttIsarRobotHeartbeatReceived, _ when type == typeof(IsarMissionMessage) => MqttIsarMissionReceived, _ when type == typeof(IsarTaskMessage) => MqttIsarTaskReceived, _ when type == typeof(IsarBatteryMessage) => MqttIsarBatteryReceived, _ when type == typeof(IsarPressureMessage) => MqttIsarPressureReceived, _ when type == typeof(IsarPoseMessage) => MqttIsarPoseReceived, _ when type == typeof(IsarCloudHealthMessage) => MqttIsarCloudHealthReceived, - _ - => throw new NotImplementedException( - $"No event defined for message type '{typeof(T).Name}'" - ) + _ => throw new NotImplementedException( + $"No event defined for message type '{typeof(T).Name}'" + ), }; // Event will be null if there are no subscribers if (raiseEvent is not null) diff --git a/backend/api/MQTT/MqttTopics.cs b/backend/api/MQTT/MqttTopics.cs index 93dae6b74..93f052905 100644 --- a/backend/api/MQTT/MqttTopics.cs +++ b/backend/api/MQTT/MqttTopics.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Api.Mqtt.MessageModels; + namespace Api.Mqtt { /// @@ -13,33 +14,15 @@ public static class MqttTopics public static readonly Dictionary TopicsToMessages = new() { - { - "isar/+/status", typeof(IsarStatusMessage) - }, - { - "isar/+/robot_info", typeof(IsarRobotInfoMessage) - }, - { - "isar/+/robot_heartbeat", typeof(IsarRobotHeartbeatMessage) - }, - { - "isar/+/mission", typeof(IsarMissionMessage) - }, - { - "isar/+/task", typeof(IsarTaskMessage) - }, - { - "isar/+/battery", typeof(IsarBatteryMessage) - }, - { - "isar/+/pressure", typeof(IsarPressureMessage) - }, - { - "isar/+/pose", typeof(IsarPoseMessage) - }, - { - "isar/+/cloud_health", typeof(IsarCloudHealthMessage) - } + { "isar/+/status", typeof(IsarStatusMessage) }, + { "isar/+/robot_info", typeof(IsarRobotInfoMessage) }, + { "isar/+/robot_heartbeat", typeof(IsarRobotHeartbeatMessage) }, + { "isar/+/mission", typeof(IsarMissionMessage) }, + { "isar/+/task", typeof(IsarTaskMessage) }, + { "isar/+/battery", typeof(IsarBatteryMessage) }, + { "isar/+/pressure", typeof(IsarPressureMessage) }, + { "isar/+/pose", typeof(IsarPoseMessage) }, + { "isar/+/cloud_health", typeof(IsarCloudHealthMessage) }, }; /// diff --git a/backend/api/Program.cs b/backend/api/Program.cs index c150ca116..f836f1756 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.Rewrite; using Microsoft.Identity.Web; + var builder = WebApplication.CreateBuilder(args); Console.WriteLine($"\nENVIRONMENT IS SET TO '{builder.Environment.EnvironmentName}'\n"); @@ -32,10 +33,7 @@ builder.Configuration.AddAzureKeyVault( new Uri(vaultUri), new DefaultAzureCredential( - new DefaultAzureCredentialOptions - { - ExcludeSharedTokenCacheCredential = true - } + new DefaultAzureCredentialOptions { ExcludeSharedTokenCacheCredential = true } ) ); } @@ -58,7 +56,6 @@ TelemetryDebugWriter.IsTracingDisabled = true; #endif - builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -97,8 +94,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); -bool useInMemoryDatabase = builder.Configuration - .GetSection("Database") +bool useInMemoryDatabase = builder + .Configuration.GetSection("Database") .GetValue("UseInMemoryDatabase"); if (useInMemoryDatabase) @@ -121,23 +118,20 @@ builder.Services.Configure(builder.Configuration.GetSection("AzureAd")); builder.Services.Configure(builder.Configuration.GetSection("Maps")); - -builder.Services - .AddControllers() - .AddJsonOptions( - options => - { - options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; - } - ); +builder + .Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; + }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.ConfigureSwagger(builder.Configuration); -builder.Services - .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) +builder + .Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd")) .EnableTokenAcquisitionToCallDownstreamApi() .AddInMemoryTokenCaches() @@ -146,66 +140,57 @@ .AddDownstreamApi(InspectionService.ServiceName, builder.Configuration.GetSection("IDA")) .AddDownstreamApi(IsarService.ServiceName, builder.Configuration.GetSection("Isar")); -builder.Services.AddAuthorizationBuilder().AddFallbackPolicy( - "RequireAuthenticatedUser", policy => policy.RequireAuthenticatedUser() -); +builder + .Services.AddAuthorizationBuilder() + .AddFallbackPolicy("RequireAuthenticatedUser", policy => policy.RequireAuthenticatedUser()); builder.Services.AddSignalR(); var app = builder.Build(); string basePath = builder.Configuration["BackendBaseRoute"] ?? ""; -app.UseSwagger( - c => - { - c.PreSerializeFilters.Add( - (swaggerDoc, httpReq) => - { - swaggerDoc.Servers = - [ - new() - { - Url = $"https://{httpReq.Host.Value}{basePath}" - }, - new() - { - Url = $"http://{httpReq.Host.Value}{basePath}" - } - ]; - } - ); - } -); -app.UseSwaggerUI( - c => - { - c.OAuthClientId(builder.Configuration["AzureAd:ClientId"]); - // The following parameter represents the "audience" of the access token. - c.OAuthAdditionalQueryStringParams( - new Dictionary +app.UseSwagger(c => +{ + c.PreSerializeFilters.Add( + (swaggerDoc, httpReq) => + { + swaggerDoc.Servers = + [ + new() { Url = $"https://{httpReq.Host.Value}{basePath}" }, + new() { Url = $"http://{httpReq.Host.Value}{basePath}" }, + ]; + } + ); +}); +app.UseSwaggerUI(c => +{ + c.OAuthClientId(builder.Configuration["AzureAd:ClientId"]); + // The following parameter represents the "audience" of the access token. + c.OAuthAdditionalQueryStringParams( + new Dictionary + { { - { - "Resource", builder.Configuration["AzureAd:ClientId"] ?? throw new ArgumentException("No Azure Ad ClientId") - } - } - ); - c.OAuthUsePkce(); - } -); + "Resource", + builder.Configuration["AzureAd:ClientId"] + ?? throw new ArgumentException("No Azure Ad ClientId") + }, + } + ); + c.OAuthUsePkce(); +}); var option = new RewriteOptions(); option.AddRedirect("^$", "swagger"); app.UseRewriter(option); string[] allowedOrigins = builder.Configuration.GetSection("AllowedOrigins").Get() ?? []; -app.UseCors( - corsBuilder => - corsBuilder - .WithOrigins(allowedOrigins) - .SetIsOriginAllowedToAllowWildcardSubdomains() - .WithExposedHeaders(QueryStringParameters.PaginationHeader) - .AllowAnyHeader() - .AllowAnyMethod() - .AllowCredentials() +app.UseCors(corsBuilder => + corsBuilder + .WithOrigins(allowedOrigins) + .SetIsOriginAllowedToAllowWildcardSubdomains() + .WithExposedHeaders(QueryStringParameters.PaginationHeader) + .AllowAnyHeader() + .AllowAnyMethod() + .AllowCredentials() ); app.UseHttpsRedirection(); @@ -213,10 +198,13 @@ app.UseAuthentication(); app.UseAuthorization(); -app.MapHub("/hub", options => -{ - options.Transports = HttpTransportType.WebSockets | HttpTransportType.LongPolling; -}); +app.MapHub( + "/hub", + options => + { + options.Transports = HttpTransportType.WebSockets | HttpTransportType.LongPolling; + } +); app.MapControllers(); diff --git a/backend/api/Services/AccessRoleService.cs b/backend/api/Services/AccessRoleService.cs index 73b1bf883..7da9bd20a 100644 --- a/backend/api/Services/AccessRoleService.cs +++ b/backend/api/Services/AccessRoleService.cs @@ -11,13 +11,20 @@ public interface IAccessRoleService public Task> GetAllowedInstallationCodes(List roles); public bool IsUserAdmin(); public bool IsAuthenticationAvailable(); - public Task Create(Installation installation, string roleName, RoleAccessLevel accessLevel); + public Task Create( + Installation installation, + string roleName, + RoleAccessLevel accessLevel + ); public Task ReadByInstallation(Installation installation); public Task> ReadAll(); public void DetachTracking(AccessRole accessRole); } - public class AccessRoleService(FlotillaDbContext context, IHttpContextAccessor httpContextAccessor) : IAccessRoleService + public class AccessRoleService( + FlotillaDbContext context, + IHttpContextAccessor httpContextAccessor + ) : IAccessRoleService { private const string SUPER_ADMIN_ROLE_NAME = "Role.Admin"; @@ -29,7 +36,10 @@ private IQueryable GetAccessRoles(bool readOnly = true) public async Task> GetAllowedInstallationCodes() { if (httpContextAccessor.HttpContext == null) - return await context.Installations.AsNoTracking().Select((i) => i.InstallationCode.ToUpperInvariant()).ToListAsync(); + return await context + .Installations.AsNoTracking() + .Select((i) => i.InstallationCode.ToUpperInvariant()) + .ToListAsync(); var roles = httpContextAccessor.HttpContext.GetRequestedRoleNames(); @@ -39,23 +49,42 @@ public async Task> GetAllowedInstallationCodes() public async Task> GetAllowedInstallationCodes(List roles) { if (roles.Contains(SUPER_ADMIN_ROLE_NAME)) - return await context.Installations.AsNoTracking().Select((i) => i.InstallationCode.ToUpperInvariant()).ToListAsync(); + return await context + .Installations.AsNoTracking() + .Select((i) => i.InstallationCode.ToUpperInvariant()) + .ToListAsync(); else - return await GetAccessRoles(readOnly: true).Include((r) => r.Installation) - .Where((r) => roles.Contains(r.RoleName)).Select((r) => r.Installation != null ? r.Installation.InstallationCode.ToUpperInvariant() : "").ToListAsync(); + return await GetAccessRoles(readOnly: true) + .Include((r) => r.Installation) + .Where((r) => roles.Contains(r.RoleName)) + .Select( + (r) => + r.Installation != null + ? r.Installation.InstallationCode.ToUpperInvariant() + : "" + ) + .ToListAsync(); } private void ThrowExceptionIfNotAdmin() { if (httpContextAccessor.HttpContext == null) - throw new HttpRequestException("Access roles can only be created in authenticated HTTP requests"); + throw new HttpRequestException( + "Access roles can only be created in authenticated HTTP requests" + ); var roles = httpContextAccessor.HttpContext.GetRequestedRoleNames(); if (!roles.Contains(SUPER_ADMIN_ROLE_NAME)) - throw new HttpRequestException("This user is not authorised to create a new access role"); + throw new HttpRequestException( + "This user is not authorised to create a new access role" + ); } - public async Task Create(Installation installation, string roleName, RoleAccessLevel accessLevel) + public async Task Create( + Installation installation, + string roleName, + RoleAccessLevel accessLevel + ) { if (accessLevel == RoleAccessLevel.ADMIN) throw new HttpRequestException("Cannot create admin roles using database services"); @@ -67,7 +96,7 @@ public async Task Create(Installation installation, string roleName, { Installation = installation, RoleName = roleName, - AccessLevel = accessLevel + AccessLevel = accessLevel, }; await context.AccessRoles.AddAsync(newAccessRole); @@ -79,13 +108,18 @@ public async Task Create(Installation installation, string roleName, public async Task ReadByInstallation(Installation installation) { ThrowExceptionIfNotAdmin(); - return await GetAccessRoles(readOnly: true).Include((r) => r.Installation).Where((r) => r.Installation.Id == installation.Id).FirstOrDefaultAsync(); + return await GetAccessRoles(readOnly: true) + .Include((r) => r.Installation) + .Where((r) => r.Installation.Id == installation.Id) + .FirstOrDefaultAsync(); } public async Task> ReadAll() { ThrowExceptionIfNotAdmin(); - return await GetAccessRoles(readOnly: true).Include((r) => r.Installation).ToListAsync(); + return await GetAccessRoles(readOnly: true) + .Include((r) => r.Installation) + .ToListAsync(); } public bool IsUserAdmin() diff --git a/backend/api/Services/ActionServices/BatteryTimeseriesService.cs b/backend/api/Services/ActionServices/BatteryTimeseriesService.cs index 72536e077..df3126d57 100644 --- a/backend/api/Services/ActionServices/BatteryTimeseriesService.cs +++ b/backend/api/Services/ActionServices/BatteryTimeseriesService.cs @@ -7,7 +7,10 @@ public interface IBatteryTimeseriesService public Task AddBatteryEntry(float batteryLevel, string isarId); } - public class BatteryTimeseriesService(ILogger logger, IRobotService robotService) : IBatteryTimeseriesService + public class BatteryTimeseriesService( + ILogger logger, + IRobotService robotService + ) : IBatteryTimeseriesService { private const double Tolerance = 1E-05D; @@ -16,7 +19,10 @@ public class BatteryTimeseriesService(ILogger logger, var robot = await robotService.ReadByIsarId(isarId, readOnly: true); if (robot == null) { - logger.LogWarning("Could not find corresponding robot for battery update on robot with ISAR id'{IsarId}'", isarId); + logger.LogWarning( + "Could not find corresponding robot for battery update on robot with ISAR id'{IsarId}'", + isarId + ); return null; } @@ -30,11 +36,19 @@ public class BatteryTimeseriesService(ILogger logger, } catch (Exception e) { - logger.LogWarning("Failed to update robot battery value for robot with ID '{isarId}'. Exception: {message}", isarId, e.Message); + logger.LogWarning( + "Failed to update robot battery value for robot with ID '{isarId}'. Exception: {message}", + isarId, + e.Message + ); return null; } - logger.LogDebug("Updated battery on robot '{RobotName}' with ISAR id '{IsarId}'", robot.Name, robot.IsarId); + logger.LogDebug( + "Updated battery on robot '{RobotName}' with ISAR id '{IsarId}'", + robot.Name, + robot.IsarId + ); return robot; } } diff --git a/backend/api/Services/ActionServices/LastMissionRunService.cs b/backend/api/Services/ActionServices/LastMissionRunService.cs index 3b4f75e8d..c5a3b13f3 100644 --- a/backend/api/Services/ActionServices/LastMissionRunService.cs +++ b/backend/api/Services/ActionServices/LastMissionRunService.cs @@ -1,16 +1,27 @@ using Api.Database.Models; + namespace Api.Services.ActionServices { public interface ILastMissionRunService { - public Task SetLastMissionRun(string missionRunId, string missionDefinitionId); + public Task SetLastMissionRun( + string missionRunId, + string missionDefinitionId + ); } - public class LastMissionRunService(IMissionDefinitionService missionDefinitionService) : ILastMissionRunService + public class LastMissionRunService(IMissionDefinitionService missionDefinitionService) + : ILastMissionRunService { - public async Task SetLastMissionRun(string missionRunId, string missionDefinitionId) + public async Task SetLastMissionRun( + string missionRunId, + string missionDefinitionId + ) { - return await missionDefinitionService.UpdateLastSuccessfulMissionRun(missionRunId, missionDefinitionId); + return await missionDefinitionService.UpdateLastSuccessfulMissionRun( + missionRunId, + missionDefinitionId + ); } } } diff --git a/backend/api/Services/ActionServices/PoseTimeseriesService.cs b/backend/api/Services/ActionServices/PoseTimeseriesService.cs index 06ba64fac..e6f50e00c 100644 --- a/backend/api/Services/ActionServices/PoseTimeseriesService.cs +++ b/backend/api/Services/ActionServices/PoseTimeseriesService.cs @@ -1,4 +1,5 @@ using Api.Database.Models; + namespace Api.Services.ActionServices { public interface IPoseTimeseriesService @@ -6,14 +7,20 @@ public interface IPoseTimeseriesService public Task AddPoseEntry(Pose pose, string isarId); } - public class PoseTimeseriesService(ILogger logger, IRobotService robotService) : IPoseTimeseriesService + public class PoseTimeseriesService( + ILogger logger, + IRobotService robotService + ) : IPoseTimeseriesService { public async Task AddPoseEntry(Pose pose, string isarId) { var robot = await robotService.ReadByIsarId(isarId, readOnly: true); if (robot == null) { - logger.LogWarning("Could not find corresponding robot for pose update on robot with ISAR id '{IsarId}'", isarId); + logger.LogWarning( + "Could not find corresponding robot for pose update on robot with ISAR id '{IsarId}'", + isarId + ); return; } @@ -23,11 +30,19 @@ public async Task AddPoseEntry(Pose pose, string isarId) } catch (Exception e) { - logger.LogWarning("Failed to update robot pose value for robot with ID '{isarId}'. Exception: {message}", isarId, e.Message); + logger.LogWarning( + "Failed to update robot pose value for robot with ID '{isarId}'. Exception: {message}", + isarId, + e.Message + ); return; } - logger.LogDebug("Updated pose on robot '{RobotName}' with ISAR id '{IsarId}'", robot.Name, robot.IsarId); + logger.LogDebug( + "Updated pose on robot '{RobotName}' with ISAR id '{IsarId}'", + robot.Name, + robot.IsarId + ); } } } diff --git a/backend/api/Services/ActionServices/PressureTimeseriesService.cs b/backend/api/Services/ActionServices/PressureTimeseriesService.cs index f993b543c..cc4afc1e0 100644 --- a/backend/api/Services/ActionServices/PressureTimeseriesService.cs +++ b/backend/api/Services/ActionServices/PressureTimeseriesService.cs @@ -7,7 +7,10 @@ public interface IPressureTimeseriesService public Task AddPressureEntry(float pressureLevel, string isarId); } - public class PressureTimeseriesService(ILogger logger, IRobotService robotService) : IPressureTimeseriesService + public class PressureTimeseriesService( + ILogger logger, + IRobotService robotService + ) : IPressureTimeseriesService { private const double Tolerance = 1E-05D; @@ -16,13 +19,19 @@ public class PressureTimeseriesService(ILogger logger var robot = await robotService.ReadByIsarId(isarId, readOnly: true); if (robot == null) { - logger.LogWarning("Could not find corresponding robot for pressure update on robot with ISAR id'{IsarId}'", isarId); + logger.LogWarning( + "Could not find corresponding robot for pressure update on robot with ISAR id'{IsarId}'", + isarId + ); return null; } try { - if (robot.PressureLevel is null || Math.Abs(pressureLevel - (float)robot.PressureLevel) > Tolerance) + if ( + robot.PressureLevel is null + || Math.Abs(pressureLevel - (float)robot.PressureLevel) > Tolerance + ) { await robotService.UpdateRobotPressureLevel(robot.Id, pressureLevel); robot.PressureLevel = pressureLevel; @@ -30,11 +39,19 @@ public class PressureTimeseriesService(ILogger logger } catch (Exception e) { - logger.LogWarning("Failed to update robot pressure value for robot with ID '{isarId}'. Exception: {message}", isarId, e.Message); + logger.LogWarning( + "Failed to update robot pressure value for robot with ID '{isarId}'. Exception: {message}", + isarId, + e.Message + ); return null; } - logger.LogDebug("Updated pressure on robot '{RobotName}' with ISAR id '{IsarId}'", robot.Name, robot.IsarId); + logger.LogDebug( + "Updated pressure on robot '{RobotName}' with ISAR id '{IsarId}'", + robot.Name, + robot.IsarId + ); return robot; } } diff --git a/backend/api/Services/ActionServices/TaskDurationService.cs b/backend/api/Services/ActionServices/TaskDurationService.cs index fca6e0afc..d414b2de6 100644 --- a/backend/api/Services/ActionServices/TaskDurationService.cs +++ b/backend/api/Services/ActionServices/TaskDurationService.cs @@ -1,6 +1,7 @@ using System.Globalization; using Api.Controllers.Models; using Api.Database.Models; + namespace Api.Services.ActionServices { public interface ITaskDurationService @@ -8,63 +9,82 @@ public interface ITaskDurationService public Task UpdateAverageDurationPerTask(RobotType robotType); } - public class TaskDurationService(ILogger logger, IConfiguration configuration, IRobotModelService robotModelService, IMissionRunService missionRunService) : ITaskDurationService + public class TaskDurationService( + ILogger logger, + IConfiguration configuration, + IRobotModelService robotModelService, + IMissionRunService missionRunService + ) : ITaskDurationService { public async Task UpdateAverageDurationPerTask(RobotType robotType) { - int timeRangeInDays = configuration.GetValue("TimeRangeForMissionDurationEstimationInDays"); - long minEpochTime = DateTimeOffset.Now - .AddDays(-timeRangeInDays) - .ToUnixTimeSeconds(); + int timeRangeInDays = configuration.GetValue( + "TimeRangeForMissionDurationEstimationInDays" + ); + long minEpochTime = DateTimeOffset.Now.AddDays(-timeRangeInDays).ToUnixTimeSeconds(); var missionRunsForEstimation = await missionRunService.ReadAll( new MissionRunQueryStringParameters { MinDesiredStartTime = minEpochTime, RobotModelType = robotType, - PageSize = QueryStringParameters.MaxPageSize + PageSize = QueryStringParameters.MaxPageSize, }, - readOnly: true); + readOnly: true + ); var model = await robotModelService.ReadByRobotType(robotType, readOnly: true); if (model is null) { - logger.LogWarning("Could not update average duration for robot model {RobotType} as the model was not found", robotType); + logger.LogWarning( + "Could not update average duration for robot model {RobotType} as the model was not found", + robotType + ); return; } await UpdateAverageDuration(missionRunsForEstimation, model); } - private async Task UpdateAverageDuration(List recentMissionRunsForModelType, RobotModel robotModel) + private async Task UpdateAverageDuration( + List recentMissionRunsForModelType, + RobotModel robotModel + ) { - if (recentMissionRunsForModelType.Any(missionRun => missionRun.Robot.Model.Type != robotModel.Type)) + if ( + recentMissionRunsForModelType.Any(missionRun => + missionRun.Robot.Model.Type != robotModel.Type + ) + ) { throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, "{0} should only include missions for this model type ('{1}')", - nameof(recentMissionRunsForModelType), robotModel.Type), + nameof(recentMissionRunsForModelType), + robotModel.Type + ), nameof(recentMissionRunsForModelType) ); } // The time spent on each tasks, not including the duration of video/audio recordings var timeSpentPerTask = recentMissionRunsForModelType - .SelectMany( - missionRun => - missionRun.Tasks - .Where(task => task.EndTime is not null && task.StartTime is not null) - .Select( - task => - (task.EndTime! - task.StartTime!).Value.TotalSeconds - - (task.Inspection?.VideoDuration ?? 0) - ) - ) + .SelectMany(missionRun => + missionRun + .Tasks.Where(task => task.EndTime is not null && task.StartTime is not null) + .Select(task => + (task.EndTime! - task.StartTime!).Value.TotalSeconds + - (task.Inspection?.VideoDuration ?? 0) + ) + ) .ToList(); // If no valid task times, return - if (timeSpentPerTask.All(time => time < 0)) { return; } + if (timeSpentPerTask.All(time => time < 0)) + { + return; + } // Percentiles to exclude when calculating average const double P1 = 0.1; @@ -78,14 +98,22 @@ private async Task UpdateAverageDuration(List recentMissionRunsForMo // Calculate average, excluding outliers by using percentiles double result = timeSpentPerTask - .Select(d => d < percentile1 ? percentile1 : d > percentile9 ? percentile9 : d) + .Select(d => + d < percentile1 ? percentile1 + : d > percentile9 ? percentile9 + : d + ) .Average(); robotModel.AverageDurationPerTag = (float)result; await robotModelService.Update(robotModel); - logger.LogInformation("Robot model '{ModelType}' - Updated average time spent per tag to {AverageTimeSpent}s", robotModel.Type, robotModel.AverageDurationPerTag); + logger.LogInformation( + "Robot model '{ModelType}' - Updated average time spent per tag to {AverageTimeSpent}s", + robotModel.Type, + robotModel.AverageDurationPerTag + ); } } } diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index 20b2b5a14..528b10b87 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -6,17 +6,25 @@ using Api.Database.Models; using Api.Utilities; using Microsoft.EntityFrameworkCore; + namespace Api.Services { public interface IAreaService { - public Task> ReadAll(AreaQueryStringParameters parameters, bool readOnly = true); + public Task> ReadAll( + AreaQueryStringParameters parameters, + bool readOnly = true + ); public Task ReadById(string id, bool readOnly = true); public Task> ReadByDeckId(string deckId, bool readOnly = true); - public Task ReadByInstallationAndName(string installationCode, string areaName, bool readOnly = true); + public Task ReadByInstallationAndName( + string installationCode, + string areaName, + bool readOnly = true + ); public Task Create(CreateAreaQuery newArea); @@ -38,10 +46,18 @@ public interface IAreaService Justification = "Entity framework does not support translating culture info to SQL calls" )] public class AreaService( - FlotillaDbContext context, IInstallationService installationService, IPlantService plantService, IDeckService deckService, - IDefaultLocalizationPoseService defaultLocalizationPoseService, IAccessRoleService accessRoleService) : IAreaService + FlotillaDbContext context, + IInstallationService installationService, + IPlantService plantService, + IDeckService deckService, + IDefaultLocalizationPoseService defaultLocalizationPoseService, + IAccessRoleService accessRoleService + ) : IAreaService { - public async Task> ReadAll(AreaQueryStringParameters parameters, bool readOnly = true) + public async Task> ReadAll( + AreaQueryStringParameters parameters, + bool readOnly = true + ) { var query = GetAreasWithSubModels(readOnly: readOnly).OrderBy(a => a.Installation); var filter = ConstructFilter(parameters); @@ -57,54 +73,111 @@ public async Task> ReadAll(AreaQueryStringParameters parameters, public async Task ReadById(string id, bool readOnly = true) { - return await GetAreas(readOnly: readOnly) - .FirstOrDefaultAsync(a => a.Id.Equals(id)); + return await GetAreas(readOnly: readOnly).FirstOrDefaultAsync(a => a.Id.Equals(id)); } public async Task> ReadByDeckId(string deckId, bool readOnly = true) { - if (deckId == null) { return []; } - return await GetAreas(readOnly: readOnly).Where(a => a.Deck != null && a.Deck.Id.Equals(deckId)).ToListAsync(); + if (deckId == null) + { + return []; + } + return await GetAreas(readOnly: readOnly) + .Where(a => a.Deck != null && a.Deck.Id.Equals(deckId)) + .ToListAsync(); } - public async Task ReadByInstallationAndName(string installationCode, string areaName, bool readOnly = true) + public async Task ReadByInstallationAndName( + string installationCode, + string areaName, + bool readOnly = true + ) { - var installation = await installationService.ReadByInstallationCode(installationCode, readOnly: true); - if (installation == null) { return null; } + var installation = await installationService.ReadByInstallationCode( + installationCode, + readOnly: true + ); + if (installation == null) + { + return null; + } - return await GetAreas(readOnly: readOnly).Where(a => - a.Installation.Id.Equals(installation.Id) && a.Name.ToLower().Equals(areaName.ToLower())).FirstOrDefaultAsync(); + return await GetAreas(readOnly: readOnly) + .Where(a => + a.Installation.Id.Equals(installation.Id) + && a.Name.ToLower().Equals(areaName.ToLower()) + ) + .FirstOrDefaultAsync(); } + public async Task> ReadByInstallation(string installationCode) { - var installation = await installationService.ReadByInstallationCode(installationCode, readOnly: true); - if (installation == null) { return []; } + var installation = await installationService.ReadByInstallationCode( + installationCode, + readOnly: true + ); + if (installation == null) + { + return []; + } - return await GetAreas().Where(a => a.Installation.Id.Equals(installation.Id)).ToListAsync(); + return await GetAreas() + .Where(a => a.Installation.Id.Equals(installation.Id)) + .ToListAsync(); } + public async Task Create(CreateAreaQuery newAreaQuery) { - - var installation = await installationService.ReadByInstallationCode(newAreaQuery.InstallationCode, readOnly: true) ?? - throw new InstallationNotFoundException($"No installation with name {newAreaQuery.InstallationCode} could be found"); - - var plant = await plantService.ReadByInstallationAndPlantCode(installation, newAreaQuery.PlantCode, readOnly: true) ?? - throw new PlantNotFoundException($"No plant with name {newAreaQuery.PlantCode} could be found"); - - var deck = await deckService.ReadByInstallationAndPlantAndName(installation, plant, newAreaQuery.DeckName, readOnly: true) ?? - throw new DeckNotFoundException($"No deck with name {newAreaQuery.DeckName} could be found"); + var installation = + await installationService.ReadByInstallationCode( + newAreaQuery.InstallationCode, + readOnly: true + ) + ?? throw new InstallationNotFoundException( + $"No installation with name {newAreaQuery.InstallationCode} could be found" + ); + + var plant = + await plantService.ReadByInstallationAndPlantCode( + installation, + newAreaQuery.PlantCode, + readOnly: true + ) + ?? throw new PlantNotFoundException( + $"No plant with name {newAreaQuery.PlantCode} could be found" + ); + + var deck = + await deckService.ReadByInstallationAndPlantAndName( + installation, + plant, + newAreaQuery.DeckName, + readOnly: true + ) + ?? throw new DeckNotFoundException( + $"No deck with name {newAreaQuery.DeckName} could be found" + ); var existingArea = await ReadByInstallationAndPlantAndDeckAndName( - installation, plant, deck, newAreaQuery.AreaName, readOnly: true); + installation, + plant, + deck, + newAreaQuery.AreaName, + readOnly: true + ); if (existingArea != null) { - throw new AreaExistsException($"Area with name {newAreaQuery.AreaName} already exists"); + throw new AreaExistsException( + $"Area with name {newAreaQuery.AreaName} already exists" + ); } DefaultLocalizationPose? defaultLocalizationPose = null; if (newAreaQuery.DefaultLocalizationPose != null) { - defaultLocalizationPose = await defaultLocalizationPoseService.Create(new DefaultLocalizationPose(newAreaQuery.DefaultLocalizationPose)); + defaultLocalizationPose = await defaultLocalizationPoseService.Create( + new DefaultLocalizationPose(newAreaQuery.DefaultLocalizationPose) + ); } var newArea = new Area @@ -114,14 +187,17 @@ public async Task Create(CreateAreaQuery newAreaQuery) MapMetadata = new MapMetadata(), Deck = deck!, Plant = plant!, - Installation = installation! + Installation = installation!, }; context.Entry(newArea.Installation).State = EntityState.Unchanged; context.Entry(newArea.Plant).State = EntityState.Unchanged; context.Entry(newArea.Deck).State = EntityState.Unchanged; - if (newArea.DefaultLocalizationPose is not null) { context.Entry(newArea.DefaultLocalizationPose).State = EntityState.Modified; } + if (newArea.DefaultLocalizationPose is not null) + { + context.Entry(newArea.DefaultLocalizationPose).State = EntityState.Modified; + } await context.Areas.AddAsync(newArea); await ApplyDatabaseUpdate(installation); @@ -139,8 +215,7 @@ public async Task Update(Area area) public async Task Delete(string id) { - var area = await GetAreas() - .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + var area = await GetAreas().FirstOrDefaultAsync(ev => ev.Id.Equals(id)); if (area is null) { return null; @@ -155,17 +230,24 @@ public async Task Update(Area area) private async Task ApplyDatabaseUpdate(Installation? installation) { var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(); - if (installation == null || accessibleInstallationCodes.Contains(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture))) + if ( + installation == null + || accessibleInstallationCodes.Contains( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + ) await context.SaveChangesAsync(); else - throw new UnauthorizedAccessException($"User does not have permission to update area in installation {installation.Name}"); + throw new UnauthorizedAccessException( + $"User does not have permission to update area in installation {installation.Name}" + ); } private IQueryable GetAreas(bool readOnly = true) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - var query = context.Areas - .Include(area => area.DefaultLocalizationPose) + var query = context + .Areas.Include(area => area.DefaultLocalizationPose) .Include(area => area.Deck) .ThenInclude(deck => deck != null ? deck.DefaultLocalizationPose : null) .Include(area => area.Deck) @@ -176,7 +258,12 @@ private IQueryable GetAreas(bool readOnly = true) .Include(area => area.Plant) .ThenInclude(plant => plant.Installation) .Include(area => area.Installation) - .Where((area) => accessibleInstallationCodes.Result.Contains(area.Installation.InstallationCode.ToUpper())); + .Where( + (area) => + accessibleInstallationCodes.Result.Contains( + area.Installation.InstallationCode.ToUpper() + ) + ); return readOnly ? query.AsNoTracking() : query.AsTracking(); } @@ -185,43 +272,64 @@ private IQueryable GetAreasWithSubModels(bool readOnly = true) var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); // Include related entities using the Include method - var query = context.Areas - .Include(a => a.Deck) - .ThenInclude(deck => deck != null ? deck.Plant : null) - .ThenInclude(plant => plant != null ? plant.Installation : null) - .Include(a => a.Plant) - .ThenInclude(plant => plant != null ? plant.Installation : null) - .Include(a => a.Installation) - .Include(a => a.DefaultLocalizationPose) - .Where(a => a.Installation != null && accessibleInstallationCodes.Result.Contains(a.Installation.InstallationCode.ToUpper())); + var query = context + .Areas.Include(a => a.Deck) + .ThenInclude(deck => deck != null ? deck.Plant : null) + .ThenInclude(plant => plant != null ? plant.Installation : null) + .Include(a => a.Plant) + .ThenInclude(plant => plant != null ? plant.Installation : null) + .Include(a => a.Installation) + .Include(a => a.DefaultLocalizationPose) + .Where(a => + a.Installation != null + && accessibleInstallationCodes.Result.Contains( + a.Installation.InstallationCode.ToUpper() + ) + ); return readOnly ? query.AsNoTracking() : query.AsTracking(); } - public async Task ReadByInstallationAndPlantAndDeckAndName(Installation installation, Plant plant, Deck deck, string areaName, bool readOnly = true) + public async Task ReadByInstallationAndPlantAndDeckAndName( + Installation installation, + Plant plant, + Deck deck, + string areaName, + bool readOnly = true + ) { - return await GetAreas(readOnly: readOnly).Where(a => - a.Deck != null && a.Deck.Id.Equals(deck.Id) && - a.Plant.Id.Equals(plant.Id) && - a.Installation.Id.Equals(installation.Id) && - a.Name.ToLower().Equals(areaName.ToLower()) - ).FirstOrDefaultAsync(); + return await GetAreas(readOnly: readOnly) + .Where(a => + a.Deck != null + && a.Deck.Id.Equals(deck.Id) + && a.Plant.Id.Equals(plant.Id) + && a.Installation.Id.Equals(installation.Id) + && a.Name.ToLower().Equals(areaName.ToLower()) + ) + .FirstOrDefaultAsync(); } private static Expression> ConstructFilter( AreaQueryStringParameters parameters ) { - Expression> installationFilter = string.IsNullOrEmpty(parameters.InstallationCode) + Expression> installationFilter = string.IsNullOrEmpty( + parameters.InstallationCode + ) ? area => true - : area => area.Deck != null && - area.Deck.Plant != null && - area.Deck.Plant.Installation != null && - area.Deck.Plant.Installation.InstallationCode.ToLower().Equals(parameters.InstallationCode.ToLower().Trim()); - - Expression> deckFilter = area => string.IsNullOrEmpty(parameters.Deck) || - (area.Deck != null && - area.Deck.Name != null && - area.Deck.Name.ToLower().Equals(parameters.Deck.ToLower().Trim())); + : area => + area.Deck != null + && area.Deck.Plant != null + && area.Deck.Plant.Installation != null + && area.Deck.Plant.Installation.InstallationCode.ToLower() + .Equals(parameters.InstallationCode.ToLower().Trim()); + + Expression> deckFilter = area => + string.IsNullOrEmpty(parameters.Deck) + || ( + area.Deck != null + && area.Deck.Name != null + && area.Deck.Name.ToLower().Equals(parameters.Deck.ToLower().Trim()) + ); var area = Expression.Parameter(typeof(Area)); @@ -235,10 +343,14 @@ AreaQueryStringParameters parameters public void DetachTracking(Area area) { - if (area.Installation != null) installationService.DetachTracking(area.Installation); - if (area.Plant != null) plantService.DetachTracking(area.Plant); - if (area.Deck != null) deckService.DetachTracking(area.Deck); - if (area.DefaultLocalizationPose != null) defaultLocalizationPoseService.DetachTracking(area.DefaultLocalizationPose); + if (area.Installation != null) + installationService.DetachTracking(area.Installation); + if (area.Plant != null) + plantService.DetachTracking(area.Plant); + if (area.Deck != null) + deckService.DetachTracking(area.Deck); + if (area.DefaultLocalizationPose != null) + defaultLocalizationPoseService.DetachTracking(area.DefaultLocalizationPose); context.Entry(area).State = EntityState.Detached; } } diff --git a/backend/api/Services/BlobService.cs b/backend/api/Services/BlobService.cs index 5c7cdf6a0..ade922552 100644 --- a/backend/api/Services/BlobService.cs +++ b/backend/api/Services/BlobService.cs @@ -7,20 +7,36 @@ using Azure.Storage.Blobs; using Azure.Storage.Blobs.Models; using Microsoft.Extensions.Options; + namespace Api.Services { public interface IBlobService { - public Task DownloadBlob(string blobName, string containerName, string accountName); + public Task DownloadBlob( + string blobName, + string containerName, + string accountName + ); public AsyncPageable FetchAllBlobs(string containerName, string accountName); - public Task UploadJsonToBlob(string json, string path, string containerName, string accountName, bool overwrite); + public Task UploadJsonToBlob( + string json, + string path, + string containerName, + string accountName, + bool overwrite + ); } - public class BlobService(ILogger logger, IOptions azureOptions) : IBlobService + public class BlobService(ILogger logger, IOptions azureOptions) + : IBlobService { - public async Task DownloadBlob(string blobName, string containerName, string accountName) + public async Task DownloadBlob( + string blobName, + string containerName, + string accountName + ) { var blobContainerClient = GetBlobContainerClient(containerName, accountName); var blobClient = blobContainerClient.GetBlobClient(blobName); @@ -32,11 +48,14 @@ public class BlobService(ILogger logger, IOptions a } catch (RequestFailedException) { - logger.LogWarning("Failed to download blob {blobName} from container {containerName}", blobName, containerName); + logger.LogWarning( + "Failed to download blob {blobName} from container {containerName}", + blobName, + containerName + ); return null; } - return memoryStream.ToArray(); } @@ -55,7 +74,13 @@ public AsyncPageable FetchAllBlobs(string containerName, string accoun } } - public async Task UploadJsonToBlob(string json, string path, string containerName, string accountName, bool overwrite = false) + public async Task UploadJsonToBlob( + string json, + string path, + string containerName, + string accountName, + bool overwrite = false + ) { var blobContainerClient = GetBlobContainerClient(containerName, accountName); @@ -63,12 +88,19 @@ public async Task UploadJsonToBlob(string json, string path, string containerNam using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(json)); - try { await blobClient.UploadAsync(memoryStream, overwrite); } + try + { + await blobClient.UploadAsync(memoryStream, overwrite); + } catch (RequestFailedException e) { if (e.Status == 404 && e.ErrorCode == "ContainerNotFound") { - logger.LogError(e, "{ErrorMessage}", $"Unable to find blob container {containerName}"); + logger.LogError( + e, + "{ErrorMessage}", + $"Unable to find blob container {containerName}" + ); throw new ConfigException($"Unable to find blob container {containerName}"); } else @@ -90,7 +122,9 @@ private BlobContainerClient GetBlobContainerClient(string containerName, string azureOptions.Value.ClientSecret ) ); - var containerClient = serviceClient.GetBlobContainerClient(containerName.ToLower(CultureInfo.CurrentCulture)); + var containerClient = serviceClient.GetBlobContainerClient( + containerName.ToLower(CultureInfo.CurrentCulture) + ); return containerClient; } } diff --git a/backend/api/Services/DeckService.cs b/backend/api/Services/DeckService.cs index b9c8e6e18..864500a87 100644 --- a/backend/api/Services/DeckService.cs +++ b/backend/api/Services/DeckService.cs @@ -5,6 +5,7 @@ using Api.Database.Models; using Api.Utilities; using Microsoft.EntityFrameworkCore; + namespace Api.Services { public interface IDeckService @@ -13,11 +14,23 @@ public interface IDeckService public Task ReadById(string id, bool readOnly = true); - public Task> ReadByInstallation(string installationCode, bool readOnly = true); + public Task> ReadByInstallation( + string installationCode, + bool readOnly = true + ); - public Task ReadByInstallationAndName(string installationCode, string deckName, bool readOnly = true); + public Task ReadByInstallationAndName( + string installationCode, + string deckName, + bool readOnly = true + ); - public Task ReadByInstallationAndPlantAndName(Installation installation, Plant plant, string deckName, bool readOnly = true); + public Task ReadByInstallationAndPlantAndName( + Installation installation, + Plant plant, + string deckName, + bool readOnly = true + ); public Task Create(CreateDeckQuery newDeck); @@ -44,7 +57,8 @@ public class DeckService( IInstallationService installationService, IPlantService plantService, IAccessRoleService accessRoleService, - ISignalRService signalRService) : IDeckService + ISignalRService signalRService + ) : IDeckService { public async Task> ReadAll(bool readOnly = true) { @@ -53,42 +67,91 @@ public async Task> ReadAll(bool readOnly = true) public async Task ReadById(string id, bool readOnly = true) { - return await GetDecks(readOnly: readOnly) - .FirstOrDefaultAsync(a => a.Id.Equals(id)); + return await GetDecks(readOnly: readOnly).FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task> ReadByInstallation(string installationCode, bool readOnly = true) + public async Task> ReadByInstallation( + string installationCode, + bool readOnly = true + ) { - var installation = await installationService.ReadByInstallationCode(installationCode, readOnly: true); - if (installation == null) { return []; } - return await GetDecks(readOnly: readOnly).Where(a => - a.Installation != null && a.Installation.Id.Equals(installation.Id)).ToListAsync(); + var installation = await installationService.ReadByInstallationCode( + installationCode, + readOnly: true + ); + if (installation == null) + { + return []; + } + return await GetDecks(readOnly: readOnly) + .Where(a => a.Installation != null && a.Installation.Id.Equals(installation.Id)) + .ToListAsync(); } - public async Task ReadByInstallationAndName(string installationCode, string deckName, bool readOnly = true) + public async Task ReadByInstallationAndName( + string installationCode, + string deckName, + bool readOnly = true + ) { - if (deckName == null) { return null; } - return await GetDecks(readOnly: readOnly).Where(a => - a.Installation != null && a.Installation.InstallationCode.ToLower().Equals(installationCode.ToLower()) && a.Name.ToLower().Equals(deckName.ToLower()) - ).FirstOrDefaultAsync(); + if (deckName == null) + { + return null; + } + return await GetDecks(readOnly: readOnly) + .Where(a => + a.Installation != null + && a.Installation.InstallationCode.ToLower().Equals(installationCode.ToLower()) + && a.Name.ToLower().Equals(deckName.ToLower()) + ) + .FirstOrDefaultAsync(); } - public async Task ReadByInstallationAndPlantAndName(Installation installation, Plant plant, string name, bool readOnly = true) + public async Task ReadByInstallationAndPlantAndName( + Installation installation, + Plant plant, + string name, + bool readOnly = true + ) { - return await GetDecks(readOnly: readOnly).Where(a => - a.Plant != null && a.Plant.Id.Equals(plant.Id) && - a.Installation != null && a.Installation.Id.Equals(installation.Id) && - a.Name.ToLower().Equals(name.ToLower()) - ).Include(d => d.Plant).Include(i => i.Installation).FirstOrDefaultAsync(); + return await GetDecks(readOnly: readOnly) + .Where(a => + a.Plant != null + && a.Plant.Id.Equals(plant.Id) + && a.Installation != null + && a.Installation.Id.Equals(installation.Id) + && a.Name.ToLower().Equals(name.ToLower()) + ) + .Include(d => d.Plant) + .Include(i => i.Installation) + .FirstOrDefaultAsync(); } public async Task Create(CreateDeckQuery newDeckQuery) { - var installation = await installationService.ReadByInstallationCode(newDeckQuery.InstallationCode, readOnly: true) ?? - throw new InstallationNotFoundException($"No installation with name {newDeckQuery.InstallationCode} could be found"); - var plant = await plantService.ReadByInstallationAndPlantCode(installation, newDeckQuery.PlantCode, readOnly: true) ?? - throw new PlantNotFoundException($"No plant with name {newDeckQuery.PlantCode} could be found"); - var existingDeck = await ReadByInstallationAndPlantAndName(installation, plant, newDeckQuery.Name, readOnly: true); + var installation = + await installationService.ReadByInstallationCode( + newDeckQuery.InstallationCode, + readOnly: true + ) + ?? throw new InstallationNotFoundException( + $"No installation with name {newDeckQuery.InstallationCode} could be found" + ); + var plant = + await plantService.ReadByInstallationAndPlantCode( + installation, + newDeckQuery.PlantCode, + readOnly: true + ) + ?? throw new PlantNotFoundException( + $"No plant with name {newDeckQuery.PlantCode} could be found" + ); + var existingDeck = await ReadByInstallationAndPlantAndName( + installation, + plant, + newDeckQuery.Name, + readOnly: true + ); if (existingDeck != null) { @@ -98,7 +161,12 @@ public async Task Create(CreateDeckQuery newDeckQuery) DefaultLocalizationPose? defaultLocalizationPose = null; if (newDeckQuery.DefaultLocalizationPose != null) { - defaultLocalizationPose = await defaultLocalizationPoseService.Create(new DefaultLocalizationPose(newDeckQuery.DefaultLocalizationPose.Value.Pose, newDeckQuery.DefaultLocalizationPose.Value.IsDockingStation)); + defaultLocalizationPose = await defaultLocalizationPoseService.Create( + new DefaultLocalizationPose( + newDeckQuery.DefaultLocalizationPose.Value.Pose, + newDeckQuery.DefaultLocalizationPose.Value.IsDockingStation + ) + ); } var deck = new Deck @@ -106,16 +174,23 @@ public async Task Create(CreateDeckQuery newDeckQuery) Name = newDeckQuery.Name, Installation = installation, Plant = plant, - DefaultLocalizationPose = defaultLocalizationPose + DefaultLocalizationPose = defaultLocalizationPose, }; context.Entry(deck.Installation).State = EntityState.Unchanged; context.Entry(deck.Plant).State = EntityState.Unchanged; - if (deck.DefaultLocalizationPose is not null) { context.Entry(deck.DefaultLocalizationPose).State = EntityState.Modified; } + if (deck.DefaultLocalizationPose is not null) + { + context.Entry(deck.DefaultLocalizationPose).State = EntityState.Modified; + } await context.Decks.AddAsync(deck); await ApplyDatabaseUpdate(deck.Installation); - _ = signalRService.SendMessageAsync("Deck created", deck.Installation, new DeckResponse(deck)); + _ = signalRService.SendMessageAsync( + "Deck created", + deck.Installation, + new DeckResponse(deck) + ); DetachTracking(deck); return deck!; } @@ -124,14 +199,17 @@ public async Task Update(Deck deck) { var entry = context.Update(deck); await ApplyDatabaseUpdate(deck.Installation); - _ = signalRService.SendMessageAsync("Deck updated", deck.Installation, new DeckResponse(deck)); + _ = signalRService.SendMessageAsync( + "Deck updated", + deck.Installation, + new DeckResponse(deck) + ); return entry.Entity; } public async Task Delete(string id) { - var deck = await GetDecks() - .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + var deck = await GetDecks().FirstOrDefaultAsync(ev => ev.Id.Equals(id)); if (deck is null) { return null; @@ -139,7 +217,11 @@ public async Task Update(Deck deck) context.Decks.Remove(deck); await ApplyDatabaseUpdate(deck.Installation); - _ = signalRService.SendMessageAsync("Deck deleted", deck.Installation, new DeckResponse(deck)); + _ = signalRService.SendMessageAsync( + "Deck deleted", + deck.Installation, + new DeckResponse(deck) + ); return deck; } @@ -147,25 +229,44 @@ public async Task Update(Deck deck) private IQueryable GetDecks(bool readOnly = true) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - var query = context.Decks.Include(p => p.Plant).ThenInclude(p => p.Installation).Include(i => i.Installation).Include(d => d.DefaultLocalizationPose) - .Where((d) => accessibleInstallationCodes.Result.Contains(d.Installation.InstallationCode.ToUpper())); + var query = context + .Decks.Include(p => p.Plant) + .ThenInclude(p => p.Installation) + .Include(i => i.Installation) + .Include(d => d.DefaultLocalizationPose) + .Where( + (d) => + accessibleInstallationCodes.Result.Contains( + d.Installation.InstallationCode.ToUpper() + ) + ); return readOnly ? query.AsNoTracking() : query.AsTracking(); } private async Task ApplyDatabaseUpdate(Installation? installation) { var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(); - if (installation == null || accessibleInstallationCodes.Contains(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture))) + if ( + installation == null + || accessibleInstallationCodes.Contains( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + ) await context.SaveChangesAsync(); else - throw new UnauthorizedAccessException($"User does not have permission to update deck in installation {installation.Name}"); + throw new UnauthorizedAccessException( + $"User does not have permission to update deck in installation {installation.Name}" + ); } public void DetachTracking(Deck deck) { - if (deck.Installation != null) installationService.DetachTracking(deck.Installation); - if (deck.Plant != null) plantService.DetachTracking(deck.Plant); - if (deck.DefaultLocalizationPose != null) defaultLocalizationPoseService.DetachTracking(deck.DefaultLocalizationPose); + if (deck.Installation != null) + installationService.DetachTracking(deck.Installation); + if (deck.Plant != null) + plantService.DetachTracking(deck.Plant); + if (deck.DefaultLocalizationPose != null) + defaultLocalizationPoseService.DetachTracking(deck.DefaultLocalizationPose); context.Entry(deck).State = EntityState.Detached; } } diff --git a/backend/api/Services/DefaultLocalizationPoseService.cs b/backend/api/Services/DefaultLocalizationPoseService.cs index 2292795d1..6058a9f37 100644 --- a/backend/api/Services/DefaultLocalizationPoseService.cs +++ b/backend/api/Services/DefaultLocalizationPoseService.cs @@ -11,9 +11,13 @@ public interface IDefaultLocalizationPoseService public abstract Task ReadById(string id, bool readOnly = true); - public abstract Task Create(DefaultLocalizationPose defaultLocalizationPose); + public abstract Task Create( + DefaultLocalizationPose defaultLocalizationPose + ); - public abstract Task Update(DefaultLocalizationPose defaultLocalizationPose); + public abstract Task Update( + DefaultLocalizationPose defaultLocalizationPose + ); public abstract Task Delete(string id); @@ -25,16 +29,21 @@ public interface IDefaultLocalizationPoseService "CA1309:Use ordinal StringComparison", Justification = "EF Core refrains from translating string comparison overloads to SQL" )] - public class DefaultLocalizationPoseService(FlotillaDbContext context) : IDefaultLocalizationPoseService + public class DefaultLocalizationPoseService(FlotillaDbContext context) + : IDefaultLocalizationPoseService { public async Task> ReadAll(bool readOnly = true) { return await GetDefaultLocalizationPoses(readOnly: readOnly).ToListAsync(); } - private IQueryable GetDefaultLocalizationPoses(bool readOnly = true) + private IQueryable GetDefaultLocalizationPoses( + bool readOnly = true + ) { - return readOnly ? context.DefaultLocalizationPoses.AsNoTracking() : context.DefaultLocalizationPoses.AsTracking(); + return readOnly + ? context.DefaultLocalizationPoses.AsNoTracking() + : context.DefaultLocalizationPoses.AsTracking(); } public async Task ReadById(string id, bool readOnly = true) @@ -43,9 +52,10 @@ private IQueryable GetDefaultLocalizationPoses(bool rea .FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task Create(DefaultLocalizationPose defaultLocalizationPose) + public async Task Create( + DefaultLocalizationPose defaultLocalizationPose + ) { - await context.DefaultLocalizationPoses.AddAsync(defaultLocalizationPose); await context.SaveChangesAsync(); @@ -53,7 +63,9 @@ public async Task Create(DefaultLocalizationPose defaul return defaultLocalizationPose; } - public async Task Update(DefaultLocalizationPose defaultLocalizationPose) + public async Task Update( + DefaultLocalizationPose defaultLocalizationPose + ) { var entry = context.Update(defaultLocalizationPose); await context.SaveChangesAsync(); diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs index fedeb0f72..01bbed87c 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -8,6 +8,7 @@ using Api.Utilities; using Microsoft.EntityFrameworkCore; using Microsoft.Identity.Abstractions; + namespace Api.Services { public interface IEchoService @@ -16,16 +17,24 @@ public interface IEchoService public Task GetMissionById(string sourceMissionId); public Task> GetTasksForMission(string missionSourceId); public Task> GetPlantInfos(); - public Task CreateOrUpdateTagInspectionMetadata(TagInspectionMetadata metadata); + public Task CreateOrUpdateTagInspectionMetadata( + TagInspectionMetadata metadata + ); } public class EchoService( - ILogger logger, IDownstreamApi echoApi, ISourceService sourceService, IStidService stidService, FlotillaDbContext context) : IEchoService + ILogger logger, + IDownstreamApi echoApi, + ISourceService sourceService, + IStidService stidService, + FlotillaDbContext context + ) : IEchoService { - public const string ServiceName = "EchoApi"; - public async Task> GetAvailableMissions(string? installationCode) + public async Task> GetAvailableMissions( + string? installationCode + ) { string relativePath = string.IsNullOrEmpty(installationCode) ? "robots/robot-plan?Status=Ready" @@ -42,9 +51,9 @@ public async Task> GetAvailableMissions(string? in response.EnsureSuccessStatusCode(); - var echoMissions = await response.Content.ReadFromJsonAsync< - List - >() ?? throw new JsonException("Failed to deserialize missions from Echo"); + var echoMissions = + await response.Content.ReadFromJsonAsync>() + ?? throw new JsonException("Failed to deserialize missions from Echo"); var availableMissions = new List(); @@ -89,26 +98,34 @@ private async Task GetEchoMission(string echoMissionId) response.EnsureSuccessStatusCode(); - var echoMission = await response.Content.ReadFromJsonAsync() ?? throw new JsonException("Failed to deserialize mission from Echo"); - var processedEchoMission = ProcessEchoMission(echoMission) ?? throw new InvalidDataException($"EchoMission with id: {echoMissionId} is invalid"); + var echoMission = + await response.Content.ReadFromJsonAsync() + ?? throw new JsonException("Failed to deserialize mission from Echo"); + var processedEchoMission = + ProcessEchoMission(echoMission) + ?? throw new InvalidDataException( + $"EchoMission with id: {echoMissionId} is invalid" + ); return processedEchoMission; } public async Task> GetTasksForMission(string missionSourceId) { var echoMission = await GetEchoMission(missionSourceId); - var missionTasks = echoMission.Tags.Select(t => MissionTasksFromEchoTag(t)).SelectMany(task => task.Result).ToList(); + var missionTasks = echoMission + .Tags.Select(t => MissionTasksFromEchoTag(t)) + .SelectMany(task => task.Result) + .ToList(); return missionTasks; } - private async Task EchoMissionToMissionDefinition(EchoMission echoMission) + private async Task EchoMissionToMissionDefinition( + EchoMission echoMission + ) { - var source = await sourceService.CheckForExistingSource(echoMission.Id) ?? await sourceService.Create( - new Source - { - SourceId = $"{echoMission.Id}", - } - ); + var source = + await sourceService.CheckForExistingSource(echoMission.Id) + ?? await sourceService.Create(new Source { SourceId = $"{echoMission.Id}" }); var missionTasks = echoMission.Tags; List missionAreas; missionAreas = missionTasks @@ -116,25 +133,37 @@ public async Task> GetTasksForMission(string missionSourceId) .Select(t => stidService.GetTagArea(t.TagId, echoMission.InstallationCode).Result) .ToList(); - var missionDeckNames = missionAreas.Where(a => a != null).Select(a => a!.Deck.Name).Distinct().ToList(); + var missionDeckNames = missionAreas + .Where(a => a != null) + .Select(a => a!.Deck.Name) + .Distinct() + .ToList(); if (missionDeckNames.Count > 1) { string joinedMissionDeckNames = string.Join(", ", [.. missionDeckNames]); - logger.LogWarning($"Mission {echoMission.Name} has tags on more than one deck. The decks are: {joinedMissionDeckNames}."); + logger.LogWarning( + $"Mission {echoMission.Name} has tags on more than one deck. The decks are: {joinedMissionDeckNames}." + ); } - var sortedAreas = missionAreas.GroupBy(i => i).OrderByDescending(grp => grp.Count()).Select(grp => grp.Key); + var sortedAreas = missionAreas + .GroupBy(i => i) + .OrderByDescending(grp => grp.Count()) + .Select(grp => grp.Key); var area = sortedAreas.First(); if (area == null && sortedAreas.Count() > 1) { - logger.LogWarning($"Most common area in mission {echoMission.Name} is null. Will use second most common area."); + logger.LogWarning( + $"Most common area in mission {echoMission.Name} is null. Will use second most common area." + ); area = sortedAreas.Skip(1).First(); - } if (area == null) { - logger.LogError($"Mission {echoMission.Name} doesn't have any tags with valid area."); + logger.LogError( + $"Mission {echoMission.Name} doesn't have any tags with valid area." + ); return null; } @@ -144,7 +173,7 @@ public async Task> GetTasksForMission(string missionSourceId) Source = source, Name = echoMission.Name, InstallationCode = echoMission.InstallationCode, - InspectionArea = area.Deck + InspectionArea = area.Deck, }; return missionDefinition; } @@ -162,14 +191,17 @@ public async Task> GetPlantInfos() ); response.EnsureSuccessStatusCode(); - var echoPlantInfoResponse = await response.Content.ReadFromJsonAsync< - List - >() ?? throw new JsonException("Failed to deserialize plant information from Echo"); + var echoPlantInfoResponse = + await response.Content.ReadFromJsonAsync>() + ?? throw new JsonException("Failed to deserialize plant information from Echo"); var installations = ProcessEchoPlantInfos(echoPlantInfoResponse); return installations; } - private static List ProcessPlanItems(List planItems, string installationCode) + private static List ProcessPlanItems( + List planItems, + string installationCode + ) { var tags = new List(); @@ -179,13 +211,18 @@ private static List ProcessPlanItems(List planItems, string i for (int i = 0; i < planItems.Count; i++) { var planItem = planItems[i]; - if (planItem.SortingOrder < 0 || planItem.SortingOrder >= planItems.Count || indices.Contains(planItem.SortingOrder)) + if ( + planItem.SortingOrder < 0 + || planItem.SortingOrder >= planItems.Count + || indices.Contains(planItem.SortingOrder) + ) inconsistentIndices = true; indices.Add(planItem.SortingOrder); if (planItem.PoseId is null) { - string message = $"Invalid EchoMission {planItem.Tag} has no associated pose id"; + string message = + $"Invalid EchoMission {planItem.Tag} has no associated pose id"; throw new InvalidDataException(message); } @@ -202,8 +239,14 @@ private static List ProcessPlanItems(List planItems, string i URL = new Uri( $"https://stid.equinor.com/{installationCode}/tag?tagNo={planItem.Tag}" ), - Inspections = planItem.SensorTypes - .Select(sensor => new EchoInspection(sensor, planItem.InspectionPoint.EnuPosition.ToPosition(), planItem.InspectionPoint.Name)).Distinct(new EchoInspectionComparer()).ToList() + Inspections = planItem + .SensorTypes.Select(sensor => new EchoInspection( + sensor, + planItem.InspectionPoint.EnuPosition.ToPosition(), + planItem.InspectionPoint.Name + )) + .Distinct(new EchoInspectionComparer()) + .ToList(), }; if (tag.Inspections.Count < 1) @@ -235,7 +278,7 @@ private static List ProcessPlanItems(List planItems, string i Name = echoMission.Name, InstallationCode = echoMission.InstallationCode, URL = new Uri($"https://echo.equinor.com/mp?editId={echoMission.Id}"), - Tags = ProcessPlanItems(echoMission.PlanItems, echoMission.InstallationCode) + Tags = ProcessPlanItems(echoMission.PlanItems, echoMission.InstallationCode), }; return mission; } @@ -265,7 +308,7 @@ List echoPlantInfoResponse var echoPlantInfo = new PlantInfo { PlantCode = plant.InstallationCode, - ProjectDescription = plant.ProjectDescription + ProjectDescription = plant.ProjectDescription, }; echoPlantInfos.Add(echoPlantInfo); @@ -275,13 +318,14 @@ List echoPlantInfoResponse public async Task> MissionTasksFromEchoTag(EchoTag echoTag) { - var inspections = echoTag.Inspections - .Select(inspection => new Inspection( + var inspections = echoTag + .Inspections.Select(inspection => new Inspection( inspectionType: inspection.InspectionType, videoDuration: inspection.TimeInSeconds, inspectionTarget: inspection.InspectionPoint, inspectionTargetName: inspection.InspectionPointName, - status: InspectionStatus.NotStarted)) + status: InspectionStatus.NotStarted + )) .ToList(); var missionTasks = new List(); @@ -289,8 +333,7 @@ public async Task> MissionTasksFromEchoTag(EchoTag echoTag) foreach (var inspection in inspections) { missionTasks.Add( - new MissionTask - ( + new MissionTask( inspection: inspection, tagLink: echoTag.URL, tagId: echoTag.TagId, @@ -301,15 +344,20 @@ public async Task> MissionTasksFromEchoTag(EchoTag echoTag) zoomDescription: await FindInspectionZoom(echoTag), status: Database.Models.TaskStatus.NotStarted, type: MissionTaskType.Inspection - )); + ) + ); } return missionTasks; } - public async Task CreateOrUpdateTagInspectionMetadata(TagInspectionMetadata metadata) + public async Task CreateOrUpdateTagInspectionMetadata( + TagInspectionMetadata metadata + ) { - var existingMetadata = await context.TagInspectionMetadata.Where(e => e.TagId == metadata.TagId).FirstOrDefaultAsync(); + var existingMetadata = await context + .TagInspectionMetadata.Where(e => e.TagId == metadata.TagId) + .FirstOrDefaultAsync(); if (existingMetadata == null) { await context.TagInspectionMetadata.AddAsync(metadata); @@ -326,7 +374,11 @@ public async Task CreateOrUpdateTagInspectionMetadata(Tag private async Task FindInspectionZoom(EchoTag echoTag) { - return (await context.TagInspectionMetadata.Where((e) => e.TagId == echoTag.TagId).FirstOrDefaultAsync())?.ZoomDescription; + return ( + await context + .TagInspectionMetadata.Where((e) => e.TagId == echoTag.TagId) + .FirstOrDefaultAsync() + )?.ZoomDescription; } } } diff --git a/backend/api/Services/EmergencyActionService.cs b/backend/api/Services/EmergencyActionService.cs index 7f7f44b2b..5b47f011b 100644 --- a/backend/api/Services/EmergencyActionService.cs +++ b/backend/api/Services/EmergencyActionService.cs @@ -1,4 +1,5 @@ using Api.Services.Events; + namespace Api.Services { public interface IEmergencyActionService @@ -10,10 +11,7 @@ public interface IEmergencyActionService public class EmergencyActionService : IEmergencyActionService { - - public EmergencyActionService() - { - } + public EmergencyActionService() { } public void SendRobotToDock(RobotEmergencyEventArgs e) { @@ -38,6 +36,5 @@ protected virtual void OnReleaseRobotFromDockTriggered(RobotEmergencyEventArgs e { ReleaseRobotFromDockTriggered?.Invoke(this, e); } - } } diff --git a/backend/api/Services/ErrorHandlingService.cs b/backend/api/Services/ErrorHandlingService.cs index 701ac808d..f81dc85a9 100644 --- a/backend/api/Services/ErrorHandlingService.cs +++ b/backend/api/Services/ErrorHandlingService.cs @@ -1,5 +1,6 @@ using Api.Database.Models; using Api.Utilities; + namespace Api.Services { public interface IErrorHandlingService @@ -7,9 +8,12 @@ public interface IErrorHandlingService public Task HandleLosingConnectionToIsar(string robotId); } - public class ErrorHandlingService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService) : IErrorHandlingService + public class ErrorHandlingService( + ILogger logger, + IRobotService robotService, + IMissionRunService missionRunService + ) : IErrorHandlingService { - public async Task HandleLosingConnectionToIsar(string robotId) { try diff --git a/backend/api/Services/Events/MissionEventArgs.cs b/backend/api/Services/Events/MissionEventArgs.cs index 14e59c01f..436b8ead5 100644 --- a/backend/api/Services/Events/MissionEventArgs.cs +++ b/backend/api/Services/Events/MissionEventArgs.cs @@ -12,7 +12,8 @@ public class RobotAvailableEventArgs(string robotId) : EventArgs public string RobotId { get; } = robotId; } - public class RobotEmergencyEventArgs(string robotId, RobotFlotillaStatus robotFlotillaStatus) : EventArgs + public class RobotEmergencyEventArgs(string robotId, RobotFlotillaStatus robotFlotillaStatus) + : EventArgs { public string RobotId { get; } = robotId; public RobotFlotillaStatus RobotFlotillaStatus { get; } = robotFlotillaStatus; diff --git a/backend/api/Services/InspectionFindingService.cs b/backend/api/Services/InspectionFindingService.cs index a6d2fa208..f83e663c2 100644 --- a/backend/api/Services/InspectionFindingService.cs +++ b/backend/api/Services/InspectionFindingService.cs @@ -4,33 +4,68 @@ namespace Api.Services { - public class InspectionFindingService(FlotillaDbContext context, IAccessRoleService accessRoleService) + public class InspectionFindingService( + FlotillaDbContext context, + IAccessRoleService accessRoleService + ) { - public async Task> RetrieveInspectionFindings(DateTime lastReportingTime, bool readOnly = true) + public async Task> RetrieveInspectionFindings( + DateTime lastReportingTime, + bool readOnly = true + ) { - var inspectionFindingsQuery = readOnly ? context.InspectionFindings.AsNoTracking() : context.InspectionFindings.AsTracking(); - return await inspectionFindingsQuery.Where(f => f.InspectionDate > lastReportingTime).ToListAsync(); + var inspectionFindingsQuery = readOnly + ? context.InspectionFindings.AsNoTracking() + : context.InspectionFindings.AsTracking(); + return await inspectionFindingsQuery + .Where(f => f.InspectionDate > lastReportingTime) + .ToListAsync(); } - public async Task GetMissionRunByIsarInspectionId(string isarTaskId, bool readOnly = true) + public async Task GetMissionRunByIsarInspectionId( + string isarTaskId, + bool readOnly = true + ) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - var query = readOnly ? context.MissionRuns.AsNoTracking() : context.MissionRuns.AsTracking(); + var query = readOnly + ? context.MissionRuns.AsNoTracking() + : context.MissionRuns.AsTracking(); #pragma warning disable CA1304 - return await query.Include(missionRun => missionRun.InspectionArea).ThenInclude(area => area != null ? area.Plant : null) - .Include(missionRun => missionRun.Robot) - .Include(missionRun => missionRun.Tasks).ThenInclude(task => task.Inspection) - .Where(missionRun => missionRun.Tasks.Any(missionTask => missionTask.Inspection != null && missionTask.Inspection.Id == isarTaskId)) - .Where((m) => m.InspectionArea == null || accessibleInstallationCodes.Result.Contains(m.InspectionArea.Installation.InstallationCode.ToUpper())) - .FirstOrDefaultAsync(); + return await query + .Include(missionRun => missionRun.InspectionArea) + .ThenInclude(area => area != null ? area.Plant : null) + .Include(missionRun => missionRun.Robot) + .Include(missionRun => missionRun.Tasks) + .ThenInclude(task => task.Inspection) + .Where(missionRun => + missionRun.Tasks.Any(missionTask => + missionTask.Inspection != null && missionTask.Inspection.Id == isarTaskId + ) + ) + .Where( + (m) => + m.InspectionArea == null + || accessibleInstallationCodes.Result.Contains( + m.InspectionArea.Installation.InstallationCode.ToUpper() + ) + ) + .FirstOrDefaultAsync(); #pragma warning restore CA1304 } - public async Task GetMissionTaskByIsarInspectionId(string isarTaskId, bool readOnly = true) + public async Task GetMissionTaskByIsarInspectionId( + string isarTaskId, + bool readOnly = true + ) { var missionRun = await GetMissionRunByIsarInspectionId(isarTaskId, readOnly: readOnly); - return missionRun?.Tasks.Where(missionTask => missionTask.Inspection != null && missionTask.Inspection.Id == isarTaskId).FirstOrDefault(); + return missionRun + ?.Tasks.Where(missionTask => + missionTask.Inspection != null && missionTask.Inspection.Id == isarTaskId + ) + .FirstOrDefault(); } } } diff --git a/backend/api/Services/InspectionService.cs b/backend/api/Services/InspectionService.cs index e9443ec34..8ca845a34 100644 --- a/backend/api/Services/InspectionService.cs +++ b/backend/api/Services/InspectionService.cs @@ -9,16 +9,26 @@ using Api.Utilities; using Microsoft.EntityFrameworkCore; using Microsoft.Identity.Abstractions; + namespace Api.Services { public interface IInspectionService { - public Task FetchInpectionImage(string inpectionName, string installationCode, string storageAccount); - public Task UpdateInspectionStatus(string isarTaskId, IsarTaskStatus isarTaskStatus); + public Task FetchInpectionImage( + string inpectionName, + string installationCode, + string storageAccount + ); + public Task UpdateInspectionStatus( + string isarTaskId, + IsarTaskStatus isarTaskStatus + ); public Task ReadByIsarTaskId(string id, bool readOnly = true); - public Task AddFinding(InspectionFindingQuery inspectionFindingsQuery, string isarTaskId); + public Task AddFinding( + InspectionFindingQuery inspectionFindingsQuery, + string isarTaskId + ); public Task GetInspectionStorageInfo(string inspectionId); - } [SuppressMessage( @@ -26,17 +36,29 @@ public interface IInspectionService "CA1309:Use ordinal StringComparison", Justification = "EF Core refrains from translating string comparison overloads to SQL" )] - public class InspectionService(FlotillaDbContext context, ILogger logger, IDownstreamApi idaApi, IAccessRoleService accessRoleService, - IBlobService blobService) : IInspectionService + public class InspectionService( + FlotillaDbContext context, + ILogger logger, + IDownstreamApi idaApi, + IAccessRoleService accessRoleService, + IBlobService blobService + ) : IInspectionService { public const string ServiceName = "IDA"; - public async Task FetchInpectionImage(string inpectionName, string installationCode, string storageAccount) + public async Task FetchInpectionImage( + string inpectionName, + string installationCode, + string storageAccount + ) { return await blobService.DownloadBlob(inpectionName, installationCode, storageAccount); } - public async Task UpdateInspectionStatus(string isarTaskId, IsarTaskStatus isarTaskStatus) + public async Task UpdateInspectionStatus( + string isarTaskId, + IsarTaskStatus isarTaskStatus + ) { var inspection = await ReadByIsarTaskId(isarTaskId, readOnly: false); if (inspection is null) @@ -54,21 +76,34 @@ public async Task UpdateInspectionStatus(string isarTaskId, IsarTask private async Task ApplyDatabaseUpdate(Installation? installation) { var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(); - if (installation == null || accessibleInstallationCodes.Contains(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture))) + if ( + installation == null + || accessibleInstallationCodes.Contains( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + ) await context.SaveChangesAsync(); else - throw new UnauthorizedAccessException($"User does not have permission to update area in installation {installation.Name}"); + throw new UnauthorizedAccessException( + $"User does not have permission to update area in installation {installation.Name}" + ); } private async Task Update(Inspection inspection) { var entry = context.Update(inspection); - var missionRun = await context.MissionRuns - .Include(missionRun => missionRun.InspectionArea).ThenInclude(area => area != null ? area.Installation : null) - .Include(missionRun => missionRun.Robot) - .Where(missionRun => missionRun.Tasks.Any(missionTask => missionTask.Inspection != null && missionTask.Inspection.Id == inspection.Id)).AsNoTracking() - .FirstOrDefaultAsync(); + var missionRun = await context + .MissionRuns.Include(missionRun => missionRun.InspectionArea) + .ThenInclude(area => area != null ? area.Installation : null) + .Include(missionRun => missionRun.Robot) + .Where(missionRun => + missionRun.Tasks.Any(missionTask => + missionTask.Inspection != null && missionTask.Inspection.Id == inspection.Id + ) + ) + .AsNoTracking() + .FirstOrDefaultAsync(); var installation = missionRun?.InspectionArea?.Installation; await ApplyDatabaseUpdate(installation); @@ -78,18 +113,28 @@ private async Task Update(Inspection inspection) public async Task ReadByIsarTaskId(string id, bool readOnly = true) { - return await GetInspections(readOnly: readOnly).FirstOrDefaultAsync(inspection => inspection.IsarTaskId != null && inspection.IsarTaskId.Equals(id)); + return await GetInspections(readOnly: readOnly) + .FirstOrDefaultAsync(inspection => + inspection.IsarTaskId != null && inspection.IsarTaskId.Equals(id) + ); } private IQueryable GetInspections(bool readOnly = true) { if (accessRoleService.IsUserAdmin() || !accessRoleService.IsAuthenticationAvailable()) - return (readOnly ? context.Inspections.AsNoTracking() : context.Inspections.AsTracking()).Include(inspection => inspection.InspectionFindings); + return ( + readOnly ? context.Inspections.AsNoTracking() : context.Inspections.AsTracking() + ).Include(inspection => inspection.InspectionFindings); else - throw new UnauthorizedAccessException($"User does not have permission to view inspections"); + throw new UnauthorizedAccessException( + $"User does not have permission to view inspections" + ); } - public async Task AddFinding(InspectionFindingQuery inspectionFindingQuery, string isarTaskId) + public async Task AddFinding( + InspectionFindingQuery inspectionFindingQuery, + string isarTaskId + ) { var inspection = await ReadByIsarTaskId(isarTaskId, readOnly: false); @@ -125,25 +170,34 @@ private IQueryable GetInspections(bool readOnly = true) if (response.StatusCode == HttpStatusCode.Accepted) { - logger.LogInformation("Inspection data storage location for inspection with Id {inspectionId} is not yet available", inspectionId); + logger.LogInformation( + "Inspection data storage location for inspection with Id {inspectionId} is not yet available", + inspectionId + ); return null; } if (response.StatusCode == HttpStatusCode.InternalServerError) { - logger.LogError("Inetrnal server error when trying to get inspection data for inspection with Id {inspectionId}", inspectionId); + logger.LogError( + "Inetrnal server error when trying to get inspection data for inspection with Id {inspectionId}", + inspectionId + ); return null; } if (response.StatusCode == HttpStatusCode.NotFound) { - logger.LogError("Could not find inspection data for inspection with Id {inspectionId}", inspectionId); + logger.LogError( + "Could not find inspection data for inspection with Id {inspectionId}", + inspectionId + ); return null; } - var inspectionData = await response.Content.ReadFromJsonAsync< - IDAInspectionDataResponse - >() ?? throw new JsonException("Failed to deserialize inspection data from IDA."); + var inspectionData = + await response.Content.ReadFromJsonAsync() + ?? throw new JsonException("Failed to deserialize inspection data from IDA."); return inspectionData; } diff --git a/backend/api/Services/InstallationService.cs b/backend/api/Services/InstallationService.cs index b520c2941..76a1cbcb5 100644 --- a/backend/api/Services/InstallationService.cs +++ b/backend/api/Services/InstallationService.cs @@ -12,7 +12,10 @@ public interface IInstallationService public abstract Task ReadById(string id, bool readOnly = true); - public abstract Task ReadByInstallationCode(string installation, bool readOnly = true); + public abstract Task ReadByInstallationCode( + string installation, + bool readOnly = true + ); public abstract Task Create(CreateInstallationQuery newInstallation); @@ -33,7 +36,10 @@ public interface IInstallationService "CA1304:Specify CultureInfo", Justification = "Entity framework does not support translating culture info to SQL calls" )] - public class InstallationService(FlotillaDbContext context, IAccessRoleService accessRoleService) : IInstallationService + public class InstallationService( + FlotillaDbContext context, + IAccessRoleService accessRoleService + ) : IInstallationService { public async Task> ReadAll(bool readOnly = true) { @@ -43,8 +49,9 @@ public async Task> ReadAll(bool readOnly = true) private IQueryable GetInstallations(bool readOnly = true) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - var query = context.Installations - .Where((i) => accessibleInstallationCodes.Result.Contains(i.InstallationCode.ToUpper())); + var query = context.Installations.Where( + (i) => accessibleInstallationCodes.Result.Contains(i.InstallationCode.ToUpper()) + ); return readOnly ? query.AsNoTracking() : query.AsTracking(); } @@ -56,10 +63,17 @@ private async Task ApplyUnprotectedDatabaseUpdate() private async Task ApplyDatabaseUpdate(Installation? installation) { var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(); - if (installation == null || accessibleInstallationCodes.Contains(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture))) + if ( + installation == null + || accessibleInstallationCodes.Contains( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + ) await context.SaveChangesAsync(); else - throw new UnauthorizedAccessException($"User does not have permission to update installation in installation {installation.Name}"); + throw new UnauthorizedAccessException( + $"User does not have permission to update installation in installation {installation.Name}" + ); } public async Task ReadById(string id, bool readOnly = true) @@ -68,24 +82,30 @@ private async Task ApplyDatabaseUpdate(Installation? installation) .FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task ReadByInstallationCode(string installationCode, bool readOnly = true) + public async Task ReadByInstallationCode( + string installationCode, + bool readOnly = true + ) { if (installationCode == null) return null; - return await GetInstallations(readOnly: readOnly).Where(a => - a.InstallationCode.ToLower().Equals(installationCode.ToLower()) - ).FirstOrDefaultAsync(); + return await GetInstallations(readOnly: readOnly) + .Where(a => a.InstallationCode.ToLower().Equals(installationCode.ToLower())) + .FirstOrDefaultAsync(); } public async Task Create(CreateInstallationQuery newInstallationQuery) { - var installation = await ReadByInstallationCode(newInstallationQuery.InstallationCode, readOnly: true); + var installation = await ReadByInstallationCode( + newInstallationQuery.InstallationCode, + readOnly: true + ); if (installation == null) { installation = new Installation { Name = newInstallationQuery.Name, - InstallationCode = newInstallationQuery.InstallationCode + InstallationCode = newInstallationQuery.InstallationCode, }; await context.Installations.AddAsync(installation); await ApplyUnprotectedDatabaseUpdate(); @@ -104,8 +124,7 @@ public async Task Update(Installation installation) public async Task Delete(string id) { - var installation = await GetInstallations() - .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + var installation = await GetInstallations().FirstOrDefaultAsync(ev => ev.Id.Equals(id)); if (installation is null) { return null; diff --git a/backend/api/Services/IsarService.cs b/backend/api/Services/IsarService.cs index 039592144..bbd567f57 100644 --- a/backend/api/Services/IsarService.cs +++ b/backend/api/Services/IsarService.cs @@ -4,6 +4,7 @@ using Api.Services.Models; using Api.Utilities; using Microsoft.Identity.Abstractions; + namespace Api.Services { public interface IIsarService @@ -21,7 +22,11 @@ public interface IIsarService public Task GetMediaStreamConfig(Robot robot); } - public class IsarService(IDownstreamApi isarApi, IMissionDefinitionService missionDefinitionService, ILogger logger) : IIsarService + public class IsarService( + IDownstreamApi isarApi, + IMissionDefinitionService missionDefinitionService, + ILogger logger + ) : IIsarService { public const string ServiceName = "IsarApi"; @@ -30,13 +35,19 @@ public async Task StartMission(Robot robot, MissionRun missionRun) string? mapName = null; if (missionRun.MissionId != null) { - var missionDefinition = await missionDefinitionService.ReadById(missionRun.MissionId); + var missionDefinition = await missionDefinitionService.ReadById( + missionRun.MissionId + ); mapName = missionDefinition?.Map?.MapName; } var isarMissionDefinition = new { - mission_definition = new IsarMissionDefinition(missionRun, mapName: mapName, includeStartPose: true) + mission_definition = new IsarMissionDefinition( + missionRun, + mapName: mapName, + includeStartPose: true + ), }; HttpResponseMessage? response; @@ -51,7 +62,10 @@ public async Task StartMission(Robot robot, MissionRun missionRun) } catch (Exception e) { - logger.LogError("Encountered an exception when making an API call to ISAR: {Message}", e.Message); + logger.LogError( + "Encountered an exception when making an API call to ISAR: {Message}", + e.Message + ); throw new IsarCommunicationException(e.Message); } @@ -102,10 +116,15 @@ public async Task StopMission(Robot robot) { (string message, int statusCode) = GetErrorDescriptionFoFailedIsarRequest(response); string errorResponse = await response.Content.ReadAsStringAsync(); - if (response.StatusCode == HttpStatusCode.Conflict && errorResponse.Contains("idle", StringComparison.CurrentCultureIgnoreCase)) + if ( + response.StatusCode == HttpStatusCode.Conflict + && errorResponse.Contains("idle", StringComparison.CurrentCultureIgnoreCase) + ) { logger.LogError("No mission was running for robot '{Id}", robot.Id); - throw new MissionNotFoundException($"No mission was running for robot {robot.Id}"); + throw new MissionNotFoundException( + $"No mission was running for robot {robot.Id}" + ); } logger.LogError("{Message}: {ErrorResponse}", message, errorResponse); @@ -196,11 +215,7 @@ public async Task ResumeMission(Robot robot) public async Task StartMoveArm(Robot robot, string armPosition) { string armPositionPath = $"schedule/move_arm/{armPosition}"; - var response = await CallApi( - HttpMethod.Post, - robot.IsarUri, - armPositionPath - ); + var response = await CallApi(HttpMethod.Post, robot.IsarUri, armPositionPath); if (!response.IsSuccessStatusCode) { @@ -270,19 +285,21 @@ private async Task CallApi( return response; } - private static (string, int) GetErrorDescriptionFoFailedIsarRequest(HttpResponseMessage response) + private static (string, int) GetErrorDescriptionFoFailedIsarRequest( + HttpResponseMessage response + ) { var statusCode = response.StatusCode; string description = (int)statusCode switch { - StatusCodes.Status408RequestTimeout - => "A timeout occurred when communicating with the ISAR state machine", - StatusCodes.Status409Conflict - => "A conflict occurred when interacting with the ISAR state machine. This could imply the state machine is in a state that does not allow the current action you attempted.", - StatusCodes.Status500InternalServerError - => "An internal server error occurred in ISAR", + StatusCodes.Status408RequestTimeout => + "A timeout occurred when communicating with the ISAR state machine", + StatusCodes.Status409Conflict => + "A conflict occurred when interacting with the ISAR state machine. This could imply the state machine is in a state that does not allow the current action you attempted.", + StatusCodes.Status500InternalServerError => + "An internal server error occurred in ISAR", StatusCodes.Status401Unauthorized => "Flotilla failed to authorize towards ISAR", - _ => $"An unexpected status code '{statusCode}' was received from ISAR" + _ => $"An unexpected status code '{statusCode}' was received from ISAR", }; return (description, (int)statusCode); @@ -291,11 +308,7 @@ private static (string, int) GetErrorDescriptionFoFailedIsarRequest(HttpResponse public async Task GetMediaStreamConfig(Robot robot) { string mediaStreamPath = $"/media/media-stream-config"; - var response = await CallApi( - HttpMethod.Get, - robot.IsarUri, - mediaStreamPath - ); + var response = await CallApi(HttpMethod.Get, robot.IsarUri, mediaStreamPath); if (!response.IsSuccessStatusCode) { @@ -314,27 +327,34 @@ public async Task GetMediaStreamConfig(Robot robot) IsarMediaConfigMessage? isarMediaConfigResponse; try { - isarMediaConfigResponse = await response.Content.ReadFromJsonAsync(); + isarMediaConfigResponse = + await response.Content.ReadFromJsonAsync(); } catch (JsonException) { - string errorMessage = $"Could not parse content from new robot media stream config. {await response.Content.ReadAsStringAsync()}"; + string errorMessage = + $"Could not parse content from new robot media stream config. {await response.Content.ReadAsStringAsync()}"; logger.LogError("{ErrorMessage}", errorMessage); throw new ConfigException(errorMessage); } if (isarMediaConfigResponse == null) { - string errorMessage = $"Parsing of robot media stream config resulted in empty config. {await response.Content.ReadAsStringAsync()}"; + string errorMessage = + $"Parsing of robot media stream config resulted in empty config. {await response.Content.ReadAsStringAsync()}"; logger.LogError("{ErrorMessage}", errorMessage); throw new ConfigException(errorMessage); } - bool parseSuccess = Enum.TryParse(isarMediaConfigResponse.MediaConnectionType, out MediaConnectionType connectionType); + bool parseSuccess = Enum.TryParse( + isarMediaConfigResponse.MediaConnectionType, + out MediaConnectionType connectionType + ); if (!parseSuccess) { - string errorMessage = $"Could not parse connection type from new robot media stream config. {isarMediaConfigResponse.MediaConnectionType}"; + string errorMessage = + $"Could not parse connection type from new robot media stream config. {isarMediaConfigResponse.MediaConnectionType}"; logger.LogError("{ErrorMessage}", errorMessage); throw new ConfigException(errorMessage); } @@ -344,7 +364,7 @@ public async Task GetMediaStreamConfig(Robot robot) Url = isarMediaConfigResponse.Url, Token = isarMediaConfigResponse.Token, RobotId = robot.Id, - MediaConnectionType = connectionType + MediaConnectionType = connectionType, }; } } diff --git a/backend/api/Services/LocalizationService.cs b/backend/api/Services/LocalizationService.cs index 4b9032abf..c1b6613ab 100644 --- a/backend/api/Services/LocalizationService.cs +++ b/backend/api/Services/LocalizationService.cs @@ -1,30 +1,46 @@ using Api.Database.Models; using Api.Utilities; + namespace Api.Services { public interface ILocalizationService { - public Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition); + public Task EnsureRobotIsOnSameInstallationAsMission( + Robot robot, + MissionDefinition missionDefinition + ); public Task RobotIsOnSameDeckAsMission(string robotId, string inspectionAreaId); } - public class LocalizationService(ILogger logger, IRobotService robotService, IInstallationService installationService, IDeckService deckService) : ILocalizationService + public class LocalizationService( + ILogger logger, + IRobotService robotService, + IInstallationService installationService, + IDeckService deckService + ) : ILocalizationService { - - public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition) + public async Task EnsureRobotIsOnSameInstallationAsMission( + Robot robot, + MissionDefinition missionDefinition + ) { - var missionInstallation = await installationService.ReadByInstallationCode(missionDefinition.InstallationCode, readOnly: true); + var missionInstallation = await installationService.ReadByInstallationCode( + missionDefinition.InstallationCode, + readOnly: true + ); if (missionInstallation is null) { - string errorMessage = $"Could not find installation for installation code {missionDefinition.InstallationCode}"; + string errorMessage = + $"Could not find installation for installation code {missionDefinition.InstallationCode}"; logger.LogError("{Message}", errorMessage); throw new InstallationNotFoundException(errorMessage); } if (robot.CurrentInstallation.Id != missionInstallation.Id) { - string errorMessage = $"The robot {robot.Name} is on installation {robot.CurrentInstallation.Name} which is not the same as the mission installation {missionInstallation.Name}"; + string errorMessage = + $"The robot {robot.Name} is on installation {robot.CurrentInstallation.Name} which is not the same as the mission installation {missionInstallation.Name}"; logger.LogError("{Message}", errorMessage); throw new RobotNotInSameInstallationAsMissionException(errorMessage); } @@ -40,19 +56,30 @@ public async Task RobotIsOnSameDeckAsMission(string robotId, string inspec throw new RobotNotFoundException(errorMessage); } - if (robot.RobotCapabilities is not null && robot.RobotCapabilities.Contains(RobotCapabilitiesEnum.auto_localize)) { return true; } + if ( + robot.RobotCapabilities is not null + && robot.RobotCapabilities.Contains(RobotCapabilitiesEnum.auto_localize) + ) + { + return true; + } if (robot.CurrentInspectionArea is null) { - const string ErrorMessage = "The robot is not associated with an inspection area and a mission may not be started"; + const string ErrorMessage = + "The robot is not associated with an inspection area and a mission may not be started"; logger.LogError("{Message}", ErrorMessage); throw new RobotCurrentAreaMissingException(ErrorMessage); } - var missionInspectionArea = await deckService.ReadById(inspectionAreaId, readOnly: true); + var missionInspectionArea = await deckService.ReadById( + inspectionAreaId, + readOnly: true + ); if (missionInspectionArea is null) { - const string ErrorMessage = "The mission does not have an associated inspection area"; + const string ErrorMessage = + "The mission does not have an associated inspection area"; logger.LogError("{Message}", ErrorMessage); throw new DeckNotFoundException(ErrorMessage); } diff --git a/backend/api/Services/MapService.cs b/backend/api/Services/MapService.cs index 9cbe7e1e6..1345f56dd 100644 --- a/backend/api/Services/MapService.cs +++ b/backend/api/Services/MapService.cs @@ -9,25 +9,40 @@ namespace Api.Services public interface IMapService { public Task FetchMapImage(string mapName, string installationCode); - public Task ChooseMapFromPositions(IList positions, string installationCode); + public Task ChooseMapFromPositions( + IList positions, + string installationCode + ); public Task ChooseMapFromMissionRunTasks(MissionRun mission); } - public class MapService(ILogger logger, - IOptions blobOptions, - IBlobService blobService) : IMapService + public class MapService( + ILogger logger, + IOptions blobOptions, + IBlobService blobService + ) : IMapService { public async Task FetchMapImage(string mapName, string installationCode) { - return await blobService.DownloadBlob(mapName, installationCode, blobOptions.Value.StorageAccount); + return await blobService.DownloadBlob( + mapName, + installationCode, + blobOptions.Value.StorageAccount + ); } - public async Task ChooseMapFromPositions(IList positions, string installationCode) + public async Task ChooseMapFromPositions( + IList positions, + string installationCode + ) { var boundaries = new Dictionary(); var imageSizes = new Dictionary(); - var blobs = blobService.FetchAllBlobs(installationCode, blobOptions.Value.StorageAccount); + var blobs = blobService.FetchAllBlobs( + installationCode, + blobOptions.Value.StorageAccount + ); await foreach (var blob in blobs) { @@ -38,7 +53,11 @@ public class MapService(ILogger logger, } catch (Exception e) when (e is FormatException || e is KeyNotFoundException) { - logger.LogWarning(e, "Failed to extract boundary and image size for {MapName}", blob.Name); + logger.LogWarning( + e, + "Failed to extract boundary and image size for {MapName}", + blob.Name + ); } } @@ -52,7 +71,7 @@ public class MapService(ILogger logger, boundaries[mostSuitableMap].As2DMatrix()[1], imageSizes[mostSuitableMap][0], imageSizes[mostSuitableMap][1] - ) + ), }; return map; } @@ -89,7 +108,11 @@ public class MapService(ILogger logger, return null; } - logger.LogInformation("Assigned map {map} to mission {mission}", mapMetadata.MapName, missionRun.Name); + logger.LogInformation( + "Assigned map {map} to mission {mission}", + mapMetadata.MapName, + missionRun.Name + ); return mapMetadata; } @@ -165,7 +188,10 @@ IList positions var mapCoverage = new Dictionary(); foreach (var boundary in boundaries) { - mapCoverage.Add(boundary.Key, FractionOfTagsWithinBoundary(boundary.Value, positions)); + mapCoverage.Add( + boundary.Key, + FractionOfTagsWithinBoundary(boundary.Value, positions) + ); } string keyOfMaxValue = mapCoverage.Aggregate((x, y) => x.Value > y.Value ? x : y).Key; @@ -180,9 +206,11 @@ IList positions private static bool TagWithinBoundary(Boundary boundary, Position position) { return position.X > boundary.X1 - && position.X < boundary.X2 && position.Y > boundary.Y1 - && position.Y < boundary.Y2 && position.Z > boundary.Z1 - && position.Z < boundary.Z2; + && position.X < boundary.X2 + && position.Y > boundary.Y1 + && position.Y < boundary.Y2 + && position.Z > boundary.Z1 + && position.Z < boundary.Z2; } private float FractionOfTagsWithinBoundary(Boundary boundary, IList positions) @@ -199,7 +227,9 @@ private float FractionOfTagsWithinBoundary(Boundary boundary, IList po } catch { - logger.LogWarning("An error occurred while checking if tag was within boundary"); + logger.LogWarning( + "An error occurred while checking if tag was within boundary" + ); } } diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index 1a4a2e113..f7fb2ddaf 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -7,6 +7,7 @@ using Api.Services.MissionLoaders; using Api.Utilities; using Microsoft.EntityFrameworkCore; + namespace Api.Services { public interface IMissionDefinitionService @@ -15,15 +16,24 @@ public interface IMissionDefinitionService public Task ReadById(string id, bool readOnly = true); - public Task> ReadAll(MissionDefinitionQueryStringParameters parameters, bool readOnly = true); + public Task> ReadAll( + MissionDefinitionQueryStringParameters parameters, + bool readOnly = true + ); - public Task> ReadByInspectionAreaId(string inspectionAreaId, bool readOnly = true); + public Task> ReadByInspectionAreaId( + string inspectionAreaId, + bool readOnly = true + ); public Task?> GetTasksFromSource(Source source); public Task> ReadBySourceId(string sourceId, bool readOnly = true); - public Task UpdateLastSuccessfulMissionRun(string missionRunId, string missionDefinitionId); + public Task UpdateLastSuccessfulMissionRun( + string missionRunId, + string missionDefinitionId + ); public Task Update(MissionDefinition missionDefinition); @@ -42,36 +52,56 @@ public interface IMissionDefinitionService "CA1304:Specify CultureInfo", Justification = "Entity framework does not support translating culture info to SQL calls" )] - public class MissionDefinitionService(FlotillaDbContext context, - IMissionLoader missionLoader, - ISignalRService signalRService, - IAccessRoleService accessRoleService, - ILogger logger, - IMissionRunService missionRunService, - ISourceService sourceService) : IMissionDefinitionService + public class MissionDefinitionService( + FlotillaDbContext context, + IMissionLoader missionLoader, + ISignalRService signalRService, + IAccessRoleService accessRoleService, + ILogger logger, + IMissionRunService missionRunService, + ISourceService sourceService + ) : IMissionDefinitionService { public async Task Create(MissionDefinition missionDefinition) { - if (missionDefinition.LastSuccessfulRun is not null) { context.Entry(missionDefinition.LastSuccessfulRun).State = EntityState.Unchanged; } - if (missionDefinition.InspectionArea is not null) { context.Entry(missionDefinition.InspectionArea).State = EntityState.Unchanged; } - if (missionDefinition.Source is not null) { context.Entry(missionDefinition.Source).State = EntityState.Unchanged; } + if (missionDefinition.LastSuccessfulRun is not null) + { + context.Entry(missionDefinition.LastSuccessfulRun).State = EntityState.Unchanged; + } + if (missionDefinition.InspectionArea is not null) + { + context.Entry(missionDefinition.InspectionArea).State = EntityState.Unchanged; + } + if (missionDefinition.Source is not null) + { + context.Entry(missionDefinition.Source).State = EntityState.Unchanged; + } await context.MissionDefinitions.AddAsync(missionDefinition); await ApplyDatabaseUpdate(missionDefinition.InspectionArea?.Installation); - _ = signalRService.SendMessageAsync("Mission definition created", missionDefinition.InspectionArea?.Installation, new MissionDefinitionResponse(missionDefinition)); + _ = signalRService.SendMessageAsync( + "Mission definition created", + missionDefinition.InspectionArea?.Installation, + new MissionDefinitionResponse(missionDefinition) + ); DetachTracking(missionDefinition); return missionDefinition; } public async Task ReadById(string id, bool readOnly = true) { - return await GetMissionDefinitionsWithSubModels(readOnly: readOnly).Where(m => m.IsDeprecated == false) + return await GetMissionDefinitionsWithSubModels(readOnly: readOnly) + .Where(m => m.IsDeprecated == false) .FirstOrDefaultAsync(missionDefinition => missionDefinition.Id.Equals(id)); } - public async Task> ReadAll(MissionDefinitionQueryStringParameters parameters, bool readOnly = true) + public async Task> ReadAll( + MissionDefinitionQueryStringParameters parameters, + bool readOnly = true + ) { - var query = GetMissionDefinitionsWithSubModels(readOnly: readOnly).Where(m => m.IsDeprecated == false); + var query = GetMissionDefinitionsWithSubModels(readOnly: readOnly) + .Where(m => m.IsDeprecated == false); var filter = ConstructFilter(parameters); query = query.Where(filter); @@ -87,19 +117,38 @@ public async Task> ReadAll(MissionDefinitionQuerySt ); } - public async Task> ReadByInspectionAreaId(string inspectionAreaId, bool readOnly = true) + public async Task> ReadByInspectionAreaId( + string inspectionAreaId, + bool readOnly = true + ) { - return await GetMissionDefinitionsWithSubModels(readOnly: readOnly).Where( - m => m.IsDeprecated == false && m.InspectionArea != null && m.InspectionArea.Id == inspectionAreaId).ToListAsync(); + return await GetMissionDefinitionsWithSubModels(readOnly: readOnly) + .Where(m => + m.IsDeprecated == false + && m.InspectionArea != null + && m.InspectionArea.Id == inspectionAreaId + ) + .ToListAsync(); } - public async Task> ReadBySourceId(string sourceId, bool readOnly = true) + public async Task> ReadBySourceId( + string sourceId, + bool readOnly = true + ) { - return await GetMissionDefinitionsWithSubModels(readOnly: readOnly).Where( - m => m.IsDeprecated == false && m.Source.SourceId != null && m.Source.SourceId == sourceId).ToListAsync(); + return await GetMissionDefinitionsWithSubModels(readOnly: readOnly) + .Where(m => + m.IsDeprecated == false + && m.Source.SourceId != null + && m.Source.SourceId == sourceId + ) + .ToListAsync(); } - public async Task UpdateLastSuccessfulMissionRun(string missionRunId, string missionDefinitionId) + public async Task UpdateLastSuccessfulMissionRun( + string missionRunId, + string missionDefinitionId + ) { var missionRun = await missionRunService.ReadById(missionRunId, readOnly: true); if (missionRun is null) @@ -118,19 +167,31 @@ public async Task UpdateLastSuccessfulMissionRun(string missi if (missionRun.Status == MissionStatus.Successful) { missionDefinition.LastSuccessfulRun = missionRun; - logger.LogInformation($"Updated last successful mission run on mission definition {missionDefinitionId} to mission run {missionRunId}"); + logger.LogInformation( + $"Updated last successful mission run on mission definition {missionDefinitionId} to mission run {missionRunId}" + ); } return await Update(missionDefinition); } public async Task Update(MissionDefinition missionDefinition) { - if (missionDefinition.LastSuccessfulRun is not null) { context.Entry(missionDefinition.LastSuccessfulRun).State = EntityState.Unchanged; } - if (missionDefinition.InspectionArea is not null) { context.Entry(missionDefinition.InspectionArea).State = EntityState.Unchanged; } + if (missionDefinition.LastSuccessfulRun is not null) + { + context.Entry(missionDefinition.LastSuccessfulRun).State = EntityState.Unchanged; + } + if (missionDefinition.InspectionArea is not null) + { + context.Entry(missionDefinition.InspectionArea).State = EntityState.Unchanged; + } var entry = context.Update(missionDefinition); await ApplyDatabaseUpdate(missionDefinition.InspectionArea?.Installation); - _ = signalRService.SendMessageAsync("Mission definition updated", missionDefinition?.InspectionArea?.Installation, missionDefinition != null ? new MissionDefinitionResponse(missionDefinition) : null); + _ = signalRService.SendMessageAsync( + "Mission definition updated", + missionDefinition?.InspectionArea?.Installation, + missionDefinition != null ? new MissionDefinitionResponse(missionDefinition) : null + ); return entry.Entity; } @@ -138,7 +199,10 @@ public async Task Update(MissionDefinition missionDefinition) { // We do not delete the source here as more than one mission definition may be using it var missionDefinition = await ReadById(id); - if (missionDefinition is null) { return null; } + if (missionDefinition is null) + { + return null; + } missionDefinition.IsDeprecated = true; await ApplyDatabaseUpdate(missionDefinition.InspectionArea?.Installation); @@ -154,18 +218,26 @@ public async Task Update(MissionDefinition missionDefinition) private async Task ApplyDatabaseUpdate(Installation? installation) { var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(); - if (installation == null || accessibleInstallationCodes.Contains(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture))) + if ( + installation == null + || accessibleInstallationCodes.Contains( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + ) await context.SaveChangesAsync(); else - throw new UnauthorizedAccessException($"User does not have permission to update mission definition in installation {installation.Name}"); - + throw new UnauthorizedAccessException( + $"User does not have permission to update mission definition in installation {installation.Name}" + ); } - private IQueryable GetMissionDefinitionsWithSubModels(bool readOnly = true) + private IQueryable GetMissionDefinitionsWithSubModels( + bool readOnly = true + ) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - var query = context.MissionDefinitions - .Include(missionDefinition => missionDefinition.InspectionArea) + var query = context + .MissionDefinitions.Include(missionDefinition => missionDefinition.InspectionArea) .ThenInclude(deck => deck!.Plant) .ThenInclude(plant => plant.Installation) .Include(missionDefinition => missionDefinition.InspectionArea) @@ -174,23 +246,35 @@ private IQueryable GetMissionDefinitionsWithSubModels(bool re .Include(missionDefinition => missionDefinition.LastSuccessfulRun) .ThenInclude(missionRun => missionRun != null ? missionRun.Tasks : null)! .ThenInclude(missionTask => missionTask.Inspection) - .ThenInclude(inspection => inspection != null ? inspection.InspectionFindings : null) + .ThenInclude(inspection => + inspection != null ? inspection.InspectionFindings : null + ) .Include(missionDefinition => missionDefinition.InspectionArea) .ThenInclude(deck => deck!.DefaultLocalizationPose) - .ThenInclude(defaultLocalizationPose => defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null) - .Where((m) => m.InspectionArea == null || accessibleInstallationCodes.Result.Contains(m.InspectionArea.Installation.InstallationCode.ToUpper())); + .ThenInclude(defaultLocalizationPose => + defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null + ) + .Where( + (m) => + m.InspectionArea == null + || accessibleInstallationCodes.Result.Contains( + m.InspectionArea.Installation.InstallationCode.ToUpper() + ) + ); return readOnly ? query.AsNoTracking() : query.AsTracking(); } - private static void SearchByName(ref IQueryable missionDefinitions, string? name) + private static void SearchByName( + ref IQueryable missionDefinitions, + string? name + ) { if (!missionDefinitions.Any() || string.IsNullOrWhiteSpace(name)) return; #pragma warning disable CA1862 - missionDefinitions = missionDefinitions.Where( - missionDefinition => - missionDefinition.Name != null && missionDefinition.Name.Contains(name.Trim()) + missionDefinitions = missionDefinitions.Where(missionDefinition => + missionDefinition.Name != null && missionDefinition.Name.Contains(name.Trim()) ); #pragma warning restore CA1862 } @@ -209,15 +293,22 @@ private static Expression> ConstructFilter( MissionDefinitionQueryStringParameters parameters ) { - Expression> inspectionAreaFilter = parameters.InspectionArea is null - ? missionDefinition => true - : missionDefinition => - missionDefinition.InspectionArea != null && missionDefinition.InspectionArea.Name.ToLower().Equals(parameters.InspectionArea.Trim().ToLower()); - - Expression> installationFilter = parameters.InstallationCode is null - ? missionDefinition => true - : missionDefinition => - missionDefinition.InstallationCode.ToLower().Equals(parameters.InstallationCode.Trim().ToLower()); + Expression> inspectionAreaFilter = + parameters.InspectionArea is null + ? missionDefinition => true + : missionDefinition => + missionDefinition.InspectionArea != null + && missionDefinition + .InspectionArea.Name.ToLower() + .Equals(parameters.InspectionArea.Trim().ToLower()); + + Expression> installationFilter = + parameters.InstallationCode is null + ? missionDefinition => true + : missionDefinition => + missionDefinition + .InstallationCode.ToLower() + .Equals(parameters.InstallationCode.Trim().ToLower()); // The parameter of the filter expression var missionDefinitionExpression = Expression.Parameter(typeof(MissionDefinition)); @@ -229,13 +320,18 @@ MissionDefinitionQueryStringParameters parameters ); // Constructing the resulting lambda expression by combining parameter and body - return Expression.Lambda>(body, missionDefinitionExpression); + return Expression.Lambda>( + body, + missionDefinitionExpression + ); } public void DetachTracking(MissionDefinition missionDefinition) { - if (missionDefinition.LastSuccessfulRun != null) missionRunService.DetachTracking(missionDefinition.LastSuccessfulRun); - if (missionDefinition.Source != null) sourceService.DetachTracking(missionDefinition.Source); + if (missionDefinition.LastSuccessfulRun != null) + missionRunService.DetachTracking(missionDefinition.LastSuccessfulRun); + if (missionDefinition.Source != null) + sourceService.DetachTracking(missionDefinition.Source); context.Entry(missionDefinition).State = EntityState.Detached; } } diff --git a/backend/api/Services/MissionLoaders/EchoAndCustomMissionLoader.cs b/backend/api/Services/MissionLoaders/EchoAndCustomMissionLoader.cs index a62f0ebd3..974ad8413 100644 --- a/backend/api/Services/MissionLoaders/EchoAndCustomMissionLoader.cs +++ b/backend/api/Services/MissionLoaders/EchoAndCustomMissionLoader.cs @@ -1,12 +1,14 @@ using Api.Controllers.Models; using Api.Database.Models; + namespace Api.Services.MissionLoaders { - public class EchoAndCustomMissionLoader( - IEchoService echoService, - ISourceService sourceService) : IMissionLoader + public class EchoAndCustomMissionLoader(IEchoService echoService, ISourceService sourceService) + : IMissionLoader { - public async Task> GetAvailableMissions(string? installationCode) + public async Task> GetAvailableMissions( + string? installationCode + ) { return await echoService.GetAvailableMissions(installationCode); } @@ -18,7 +20,9 @@ public async Task> GetAvailableMissions(string? in public async Task> GetTasksForMission(string missionSourceId) { - var customMissionTasks = await sourceService.GetMissionTasksFromSourceId(missionSourceId); + var customMissionTasks = await sourceService.GetMissionTasksFromSourceId( + missionSourceId + ); if (customMissionTasks != null) { return customMissionTasks; diff --git a/backend/api/Services/MissionLoaders/EchoInspection.cs b/backend/api/Services/MissionLoaders/EchoInspection.cs index 9f2cca306..0cba1a442 100644 --- a/backend/api/Services/MissionLoaders/EchoInspection.cs +++ b/backend/api/Services/MissionLoaders/EchoInspection.cs @@ -1,22 +1,27 @@ using Api.Controllers.Models; using Api.Database.Models; + namespace Api.Services.MissionLoaders { public class EchoInspection { - public EchoInspection() { InspectionType = InspectionType.Image; InspectionPoint = new Position(); } - public EchoInspection(SensorType echoSensorType, Position inspectionPoint, string? inspectionPointName) + public EchoInspection( + SensorType echoSensorType, + Position inspectionPoint, + string? inspectionPointName + ) { InspectionType = InspectionTypeFromEchoSensorType(echoSensorType.Key); TimeInSeconds = (float?)echoSensorType.TimeInSeconds; InspectionPoint = inspectionPoint; - InspectionPointName = inspectionPointName != "Stid Coordinate" ? inspectionPointName : null; + InspectionPointName = + inspectionPointName != "Stid Coordinate" ? inspectionPointName : null; } public InspectionType InspectionType { get; set; } @@ -38,10 +43,9 @@ private static InspectionType InspectionTypeFromEchoSensorType(string sensorType "Video" => InspectionType.Video, "ThermicVideo" => InspectionType.ThermalVideo, "ThermalVideo" => InspectionType.ThermalVideo, - _ - => throw new InvalidDataException( - $"Echo sensor type '{sensorType}' not supported" - ) + _ => throw new InvalidDataException( + $"Echo sensor type '{sensorType}' not supported" + ), }; } } @@ -50,13 +54,19 @@ public class EchoInspectionComparer : IEqualityComparer { public bool Equals(EchoInspection? e1, EchoInspection? e2) { - if (ReferenceEquals(e1, e2)) { return true; } + if (ReferenceEquals(e1, e2)) + { + return true; + } - if (e2 is null || e1 is null) { return false; } + if (e2 is null || e1 is null) + { + return false; + } return e1.InspectionType == e2.InspectionType - && e1.TimeInSeconds == e2.TimeInSeconds - && e1.InspectionPoint.Equals(e2.InspectionPoint); + && e1.TimeInSeconds == e2.TimeInSeconds + && e1.InspectionPoint.Equals(e2.InspectionPoint); } public int GetHashCode(EchoInspection e) diff --git a/backend/api/Services/MissionLoaders/EchoMissionLoader.cs b/backend/api/Services/MissionLoaders/EchoMissionLoader.cs index 3c24e1330..a66ad436a 100644 --- a/backend/api/Services/MissionLoaders/EchoMissionLoader.cs +++ b/backend/api/Services/MissionLoaders/EchoMissionLoader.cs @@ -1,12 +1,13 @@ using Api.Controllers.Models; using Api.Database.Models; + namespace Api.Services.MissionLoaders { - public class EchoMissionLoader( - IEchoService echoService - ) : IMissionLoader + public class EchoMissionLoader(IEchoService echoService) : IMissionLoader { - public async Task> GetAvailableMissions(string? installationCode) + public async Task> GetAvailableMissions( + string? installationCode + ) { return await echoService.GetAvailableMissions(installationCode); } diff --git a/backend/api/Services/MissionLoaders/EchoMissionResponse.cs b/backend/api/Services/MissionLoaders/EchoMissionResponse.cs index c2137eccb..e6e94f142 100644 --- a/backend/api/Services/MissionLoaders/EchoMissionResponse.cs +++ b/backend/api/Services/MissionLoaders/EchoMissionResponse.cs @@ -1,6 +1,7 @@ # nullable disable using System.Text.Json.Serialization; using Api.Services.Models; + namespace Api.Controllers.Models { public class EchoMissionResponse diff --git a/backend/api/Services/MissionLoaders/MissionLoaderInterface.cs b/backend/api/Services/MissionLoaders/MissionLoaderInterface.cs index a2b9b1d24..d976cba60 100644 --- a/backend/api/Services/MissionLoaders/MissionLoaderInterface.cs +++ b/backend/api/Services/MissionLoaders/MissionLoaderInterface.cs @@ -1,5 +1,6 @@ using Api.Controllers.Models; using Api.Database.Models; + namespace Api.Services.MissionLoaders { public interface IMissionLoader diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index 127f075cd..4b40f3803 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -9,13 +9,20 @@ using Api.Services.Models; using Api.Utilities; using Microsoft.EntityFrameworkCore; + namespace Api.Services { public interface IMissionRunService { - public Task Create(MissionRun missionRun, bool triggerCreatedMissionRunEvent = true); + public Task Create( + MissionRun missionRun, + bool triggerCreatedMissionRunEvent = true + ); - public Task> ReadAll(MissionRunQueryStringParameters parameters, bool readOnly = true); + public Task> ReadAll( + MissionRunQueryStringParameters parameters, + bool readOnly = true + ); public Task ReadById(string id, bool readOnly = true); @@ -23,21 +30,38 @@ public interface IMissionRunService public Task> ReadMissionRunQueue(string robotId, bool readOnly = true); - public Task ReadNextScheduledRunByMissionId(string missionId, bool readOnly = true); + public Task ReadNextScheduledRunByMissionId( + string missionId, + bool readOnly = true + ); public Task ReadNextScheduledMissionRun(string robotId, bool readOnly = true); - public Task ReadNextScheduledEmergencyMissionRun(string robotId, bool readOnly = true); + public Task ReadNextScheduledEmergencyMissionRun( + string robotId, + bool readOnly = true + ); - public Task> ReadMissionRuns(string robotId, MissionRunType? missionRunType, IList? filterStatuses = null, bool readOnly = true); + public Task> ReadMissionRuns( + string robotId, + MissionRunType? missionRunType, + IList? filterStatuses = null, + bool readOnly = true + ); - public Task ReadLastExecutedMissionRunByRobot(string robotId, bool readOnly = true); + public Task ReadLastExecutedMissionRunByRobot( + string robotId, + bool readOnly = true + ); public Task PendingOrOngoingReturnToHomeMissionRunExists(string robotId); public bool IncludesUnsupportedInspectionType(MissionRun missionRun); - public Task UpdateMissionRunType(string missionRunId, MissionRunType missionRunType); + public Task UpdateMissionRunType( + string missionRunId, + MissionRunType missionRunType + ); public Task UpdateMissionRunStatusByIsarMissionId( string isarMissionId, @@ -46,11 +70,18 @@ MissionStatus missionStatus public Task Delete(string id); - public Task UpdateMissionRunProperty(string missionRunId, string propertyName, object? value); + public Task UpdateMissionRunProperty( + string missionRunId, + string propertyName, + object? value + ); public Task UpdateWithIsarInfo(string missionRunId, IsarMission isarMission); - public Task SetMissionRunToFailed(string missionRunId, string failureDescription); + public Task SetMissionRunToFailed( + string missionRunId, + string failureDescription + ); public Task UpdateCurrentRobotMissionToFailed(string robotId); @@ -75,23 +106,39 @@ public class MissionRunService( IMissionTaskService missionTaskService, IDeckService deckService, IRobotService robotService, - IUserInfoService userInfoService) : IMissionRunService + IUserInfoService userInfoService + ) : IMissionRunService { - public async Task Create(MissionRun missionRun, bool triggerCreatedMissionRunEvent = true) + public async Task Create( + MissionRun missionRun, + bool triggerCreatedMissionRunEvent = true + ) { missionRun.Id ??= Guid.NewGuid().ToString(); // Useful for signalR messages if (IncludesUnsupportedInspectionType(missionRun)) { - throw new UnsupportedRobotCapabilityException($"Mission {missionRun.Name} contains inspection types not supported by robot: {missionRun.Robot.Name}."); + throw new UnsupportedRobotCapabilityException( + $"Mission {missionRun.Name} contains inspection types not supported by robot: {missionRun.Robot.Name}." + ); } - if (missionRun.InspectionArea is not null) { context.Entry(missionRun.InspectionArea).State = EntityState.Unchanged; } - if (missionRun.Robot is not null) { context.Entry(missionRun.Robot).State = EntityState.Unchanged; } + if (missionRun.InspectionArea is not null) + { + context.Entry(missionRun.InspectionArea).State = EntityState.Unchanged; + } + if (missionRun.Robot is not null) + { + context.Entry(missionRun.Robot).State = EntityState.Unchanged; + } await context.MissionRuns.AddAsync(missionRun); await ApplyDatabaseUpdate(missionRun.InspectionArea?.Installation); - _ = signalRService.SendMessageAsync("Mission run created", missionRun.InspectionArea?.Installation, new MissionRunResponse(missionRun)); + _ = signalRService.SendMessageAsync( + "Mission run created", + missionRun.InspectionArea?.Installation, + new MissionRunResponse(missionRun) + ); if (triggerCreatedMissionRunEvent) { @@ -100,14 +147,20 @@ public async Task Create(MissionRun missionRun, bool triggerCreatedM } var userInfo = await userInfoService.GetRequestedUserInfo(); - if (userInfo != null) { logger.LogInformation($"Mission run created by user with Id {userInfo.Id}"); } + if (userInfo != null) + { + logger.LogInformation($"Mission run created by user with Id {userInfo.Id}"); + } DetachTracking(missionRun); return missionRun; } - public async Task> ReadAll(MissionRunQueryStringParameters parameters, bool readOnly = true) + public async Task> ReadAll( + MissionRunQueryStringParameters parameters, + bool readOnly = true + ) { var query = GetMissionRunsWithSubModels(readOnly: readOnly); var filter = ConstructFilter(parameters); @@ -133,38 +186,61 @@ public async Task> ReadAll(MissionRunQueryStringParameters .FirstOrDefaultAsync(missionRun => missionRun.Id.Equals(id)); } - public async Task> ReadMissionRunQueue(string robotId, bool readOnly = true) + public async Task> ReadMissionRunQueue( + string robotId, + bool readOnly = true + ) { return await GetMissionRunsWithSubModels(readOnly: readOnly) - .Where(missionRun => missionRun.Robot.Id == robotId && missionRun.Status == MissionStatus.Pending) + .Where(missionRun => + missionRun.Robot.Id == robotId && missionRun.Status == MissionStatus.Pending + ) .OrderBy(missionRun => missionRun.DesiredStartTime) .ToListAsync(); } - public async Task ReadNextScheduledMissionRun(string robotId, bool readOnly = true) + public async Task ReadNextScheduledMissionRun( + string robotId, + bool readOnly = true + ) { return await GetMissionRunsWithSubModels(readOnly: readOnly) .OrderBy(missionRun => missionRun.DesiredStartTime) - .FirstOrDefaultAsync(missionRun => missionRun.Robot.Id == robotId && missionRun.Status == MissionStatus.Pending); + .FirstOrDefaultAsync(missionRun => + missionRun.Robot.Id == robotId && missionRun.Status == MissionStatus.Pending + ); } - public async Task ReadNextScheduledEmergencyMissionRun(string robotId, bool readOnly = true) + public async Task ReadNextScheduledEmergencyMissionRun( + string robotId, + bool readOnly = true + ) { return await GetMissionRunsWithSubModels(readOnly: readOnly) .OrderBy(missionRun => missionRun.DesiredStartTime) .FirstOrDefaultAsync(missionRun => - missionRun.Robot.Id == robotId && missionRun.MissionRunType == MissionRunType.Emergency && missionRun.Status == MissionStatus.Pending); + missionRun.Robot.Id == robotId + && missionRun.MissionRunType == MissionRunType.Emergency + && missionRun.Status == MissionStatus.Pending + ); } - public async Task> ReadMissionRuns(string robotId, MissionRunType? missionRunType, IList? filterStatuses = null, bool readOnly = true) + public async Task> ReadMissionRuns( + string robotId, + MissionRunType? missionRunType, + IList? filterStatuses = null, + bool readOnly = true + ) { - var missionFilter = ConstructFilter(new MissionRunQueryStringParameters - { - Statuses = filterStatuses as List ?? null, - RobotId = robotId, - MissionRunType = missionRunType, - PageSize = 100 - }); + var missionFilter = ConstructFilter( + new MissionRunQueryStringParameters + { + Statuses = filterStatuses as List ?? null, + RobotId = robotId, + MissionRunType = missionRunType, + PageSize = 100, + } + ); return await GetMissionRunsWithSubModels(readOnly: readOnly) .Where(missionFilter) @@ -172,7 +248,10 @@ public async Task> ReadMissionRuns(string robotId, MissionRunT .ToListAsync(); } - public async Task ReadNextScheduledRunByMissionId(string missionId, bool readOnly = true) + public async Task ReadNextScheduledRunByMissionId( + string missionId, + bool readOnly = true + ) { return await GetMissionRunsWithSubModels(readOnly: readOnly) .Where(m => m.MissionId == missionId && m.EndTime == null) @@ -180,7 +259,10 @@ public async Task> ReadMissionRuns(string robotId, MissionRunT .FirstOrDefaultAsync(); } - public async Task ReadLastExecutedMissionRunByRobot(string robotId, bool readOnly = true) + public async Task ReadLastExecutedMissionRunByRobot( + string robotId, + bool readOnly = true + ) { return await GetMissionRunsWithSubModels(readOnly: readOnly) .Where(m => m.Robot.Id == robotId) @@ -192,10 +274,13 @@ public async Task> ReadMissionRuns(string robotId, MissionRunT public async Task PendingOrOngoingReturnToHomeMissionRunExists(string robotId) { var pendingMissionRuns = await ReadMissionRunQueue(robotId, readOnly: true); - if (pendingMissionRuns.Any((m) => m.IsReturnHomeMission())) return true; + if (pendingMissionRuns.Any((m) => m.IsReturnHomeMission())) + return true; var ongoingMissionRuns = await GetMissionRunsWithSubModels(readOnly: true) - .Where(missionRun => missionRun.Robot.Id == robotId && missionRun.Status == MissionStatus.Ongoing) + .Where(missionRun => + missionRun.Robot.Id == robotId && missionRun.Status == MissionStatus.Ongoing + ) .OrderBy(missionRun => missionRun.DesiredStartTime) .ToListAsync(); return ongoingMissionRuns.Any((m) => m.IsReturnHomeMission()); @@ -203,19 +288,30 @@ public async Task PendingOrOngoingReturnToHomeMissionRunExists(string robo public bool IncludesUnsupportedInspectionType(MissionRun missionRun) { - if (missionRun.Robot.RobotCapabilities == null) return false; + if (missionRun.Robot.RobotCapabilities == null) + return false; - return missionRun.Tasks.Any(task => task.Inspection != null && !task.Inspection.IsSupportedInspectionType(missionRun.Robot.RobotCapabilities)); + return missionRun.Tasks.Any(task => + task.Inspection != null + && !task.Inspection.IsSupportedInspectionType(missionRun.Robot.RobotCapabilities) + ); } public async Task Update(MissionRun missionRun) { context.Entry(missionRun.Robot).State = EntityState.Unchanged; - if (missionRun.InspectionArea is not null) { context.Entry(missionRun.InspectionArea).State = EntityState.Unchanged; } + if (missionRun.InspectionArea is not null) + { + context.Entry(missionRun.InspectionArea).State = EntityState.Unchanged; + } var entry = context.Update(missionRun); await ApplyDatabaseUpdate(missionRun.InspectionArea?.Installation); - _ = signalRService.SendMessageAsync("Mission run updated", missionRun?.InspectionArea?.Installation, missionRun != null ? new MissionRunResponse(missionRun) : null); + _ = signalRService.SendMessageAsync( + "Mission run updated", + missionRun?.InspectionArea?.Installation, + missionRun != null ? new MissionRunResponse(missionRun) : null + ); DetachTracking(missionRun!); return entry.Entity; } @@ -230,7 +326,11 @@ public async Task Update(MissionRun missionRun) } await UpdateMissionRunProperty(missionRun.Id, "IsDeprecated", true); - _ = signalRService.SendMessageAsync("Mission run deleted", missionRun?.InspectionArea?.Installation, missionRun != null ? new MissionRunResponse(missionRun) : null); + _ = signalRService.SendMessageAsync( + "Mission run deleted", + missionRun?.InspectionArea?.Installation, + missionRun != null ? new MissionRunResponse(missionRun) : null + ); return missionRun; } @@ -238,8 +338,8 @@ public async Task Update(MissionRun missionRun) private IQueryable GetMissionRunsWithSubModels(bool readOnly = true) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - var query = context.MissionRuns - .Include(missionRun => missionRun.InspectionArea) + var query = context + .MissionRuns.Include(missionRun => missionRun.InspectionArea) .ThenInclude(deck => deck != null ? deck.Plant : null) .ThenInclude(plant => plant != null ? plant.Installation : null) .Include(missionRun => missionRun.InspectionArea) @@ -249,7 +349,9 @@ private IQueryable GetMissionRunsWithSubModels(bool readOnly = true) .ThenInclude(area => area != null ? area.Installation : null) .Include(missionRun => missionRun.InspectionArea) .ThenInclude(deck => deck != null ? deck.DefaultLocalizationPose : null) - .ThenInclude(defaultLocalizationPose => defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null) + .ThenInclude(defaultLocalizationPose => + defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null + ) .Include(missionRun => missionRun.Robot) .Include(missionRun => missionRun.Robot) .ThenInclude(robot => robot.CurrentInspectionArea) @@ -262,10 +364,18 @@ private IQueryable GetMissionRunsWithSubModels(bool readOnly = true) .ThenInclude(robot => robot.Model) .Include(missionRun => missionRun.Tasks) .ThenInclude(task => task.Inspection) - .ThenInclude(inspections => inspections != null ? inspections.InspectionFindings : null) + .ThenInclude(inspections => + inspections != null ? inspections.InspectionFindings : null + ) .Include(missionRun => missionRun.Robot) .ThenInclude(robot => robot.CurrentInstallation) - .Where((m) => m.InspectionArea == null || accessibleInstallationCodes.Result.Contains(m.InspectionArea.Installation.InstallationCode.ToUpper())) + .Where( + (m) => + m.InspectionArea == null + || accessibleInstallationCodes.Result.Contains( + m.InspectionArea.Installation.InstallationCode.ToUpper() + ) + ) .Where((m) => m.IsDeprecated == false); return readOnly ? query.AsNoTracking() : query.AsTracking(); } @@ -280,10 +390,17 @@ protected virtual void OnMissionRunCreated(MissionRunCreatedEventArgs e) private async Task ApplyDatabaseUpdate(Installation? installation) { var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(); - if (installation == null || accessibleInstallationCodes.Contains(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture))) + if ( + installation == null + || accessibleInstallationCodes.Contains( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + ) await context.SaveChangesAsync(); else - throw new UnauthorizedAccessException($"User does not have permission to update mission run in installation {installation.Name}"); + throw new UnauthorizedAccessException( + $"User does not have permission to update mission run in installation {installation.Name}" + ); } private static void SearchByName(ref IQueryable missionRuns, string? name) @@ -294,37 +411,42 @@ private static void SearchByName(ref IQueryable missionRuns, string? } #pragma warning disable CA1862 - missionRuns = missionRuns.Where( - missionRun => - missionRun.Name != null && missionRun.Name.ToLower().Contains(name.ToLower().Trim()) + missionRuns = missionRuns.Where(missionRun => + missionRun.Name != null && missionRun.Name.ToLower().Contains(name.ToLower().Trim()) ); #pragma warning restore CA1862 } - private static void SearchByRobotName(ref IQueryable missionRuns, string? robotName) + private static void SearchByRobotName( + ref IQueryable missionRuns, + string? robotName + ) { - if (!missionRuns.Any() || string.IsNullOrWhiteSpace(robotName)) { return; } + if (!missionRuns.Any() || string.IsNullOrWhiteSpace(robotName)) + { + return; + } #pragma warning disable CA1862 - missionRuns = missionRuns.Where( - missionRun => missionRun.Robot.Name.ToLower().Contains(robotName.ToLower().Trim()) + missionRuns = missionRuns.Where(missionRun => + missionRun.Robot.Name.ToLower().Contains(robotName.ToLower().Trim()) ); #pragma warning restore CA1862 } private static void SearchByTag(ref IQueryable missionRuns, string? tag) { - if (!missionRuns.Any() || string.IsNullOrWhiteSpace(tag)) { return; } + if (!missionRuns.Any() || string.IsNullOrWhiteSpace(tag)) + { + return; + } - missionRuns = missionRuns.Where( - missionRun => - missionRun.Tasks.Any( - task => + missionRuns = missionRuns.Where(missionRun => + missionRun.Tasks.Any(task => #pragma warning disable CA1307 - task.TagId != null - && task.TagId.Contains(tag.Trim()) + task.TagId != null && task.TagId.Contains(tag.Trim()) #pragma warning restore CA1307 - ) + ) ); } @@ -355,16 +477,22 @@ private static Expression> ConstructFilter( MissionRunQueryStringParameters parameters ) { - Expression> inspectionAreaFilter = parameters.InspectionArea is null + Expression> inspectionAreaFilter = parameters.InspectionArea + is null ? missionRun => true : missionRun => - missionRun.InspectionArea != null && - missionRun.InspectionArea.Name.ToLower().Equals(parameters.InspectionArea.Trim().ToLower()); + missionRun.InspectionArea != null + && missionRun + .InspectionArea.Name.ToLower() + .Equals(parameters.InspectionArea.Trim().ToLower()); - Expression> installationFilter = parameters.InstallationCode is null + Expression> installationFilter = parameters.InstallationCode + is null ? missionRun => true : missionRun => - missionRun.InstallationCode.ToLower().Equals(parameters.InstallationCode.Trim().ToLower()); + missionRun + .InstallationCode.ToLower() + .Equals(parameters.InstallationCode.Trim().ToLower()); Expression> statusFilter = parameters.Statuses is null ? mission => true @@ -380,38 +508,55 @@ MissionRunQueryStringParameters parameters Expression> missionIdFilter = parameters.MissionId is null ? missionRun => true - : missionRun => missionRun.MissionId != null && missionRun.MissionId.Equals(parameters.MissionId); + : missionRun => + missionRun.MissionId != null + && missionRun.MissionId.Equals(parameters.MissionId); Expression> missionTypeFilter = parameters.MissionRunType is null ? missionRun => true : missionRun => missionRun.MissionRunType.Equals(parameters.MissionRunType); - Expression> inspectionTypeFilter = parameters.InspectionTypes is null + Expression> inspectionTypeFilter = parameters.InspectionTypes + is null ? mission => true - : mission => mission.Tasks.Any( - task => task.Inspection != null && parameters.InspectionTypes.Contains(task.Inspection.InspectionType) - ); + : mission => + mission.Tasks.Any(task => + task.Inspection != null + && parameters.InspectionTypes.Contains(task.Inspection.InspectionType) + ); Expression> returnTohomeFilter = !parameters.ExcludeReturnToHome ? missionRun => true - : missionRun => !(missionRun.Tasks.Count() == 1 && missionRun.Tasks.All(task => task.Type == MissionTaskType.ReturnHome)); + : missionRun => + !( + missionRun.Tasks.Count() == 1 + && missionRun.Tasks.All(task => task.Type == MissionTaskType.ReturnHome) + ); var minStartTime = DateTimeUtilities.UnixTimeStampToDateTime(parameters.MinStartTime); var maxStartTime = DateTimeUtilities.UnixTimeStampToDateTime(parameters.MaxStartTime); Expression> startTimeFilter = missionRun => missionRun.StartTime == null - || (DateTime.Compare(missionRun.StartTime.Value, minStartTime) >= 0 - && DateTime.Compare(missionRun.StartTime.Value, maxStartTime) <= 0); + || ( + DateTime.Compare(missionRun.StartTime.Value, minStartTime) >= 0 + && DateTime.Compare(missionRun.StartTime.Value, maxStartTime) <= 0 + ); var minEndTime = DateTimeUtilities.UnixTimeStampToDateTime(parameters.MinEndTime); var maxEndTime = DateTimeUtilities.UnixTimeStampToDateTime(parameters.MaxEndTime); Expression> endTimeFilter = missionRun => missionRun.EndTime == null - || (DateTime.Compare(missionRun.EndTime.Value, minEndTime) >= 0 - && DateTime.Compare(missionRun.EndTime.Value, maxEndTime) <= 0); + || ( + DateTime.Compare(missionRun.EndTime.Value, minEndTime) >= 0 + && DateTime.Compare(missionRun.EndTime.Value, maxEndTime) <= 0 + ); - var minDesiredStartTime = DateTimeUtilities.UnixTimeStampToDateTime(parameters.MinDesiredStartTime); - var maxDesiredStartTime = DateTimeUtilities.UnixTimeStampToDateTime(parameters.MaxDesiredStartTime); + var minDesiredStartTime = DateTimeUtilities.UnixTimeStampToDateTime( + parameters.MinDesiredStartTime + ); + var maxDesiredStartTime = DateTimeUtilities.UnixTimeStampToDateTime( + parameters.MaxDesiredStartTime + ); Expression> desiredStartTimeFilter = missionRun => DateTime.Compare(missionRun.DesiredStartTime, minDesiredStartTime) >= 0 && DateTime.Compare(missionRun.DesiredStartTime, maxDesiredStartTime) <= 0; @@ -441,8 +586,14 @@ MissionRunQueryStringParameters parameters Expression.AndAlso( Expression.Invoke(endTimeFilter, missionRun), Expression.AndAlso( - Expression.Invoke(robotTypeFilter, missionRun), - Expression.Invoke(inspectionAreaFilter, missionRun) + Expression.Invoke( + robotTypeFilter, + missionRun + ), + Expression.Invoke( + inspectionAreaFilter, + missionRun + ) ) ) ) @@ -461,16 +612,22 @@ MissionRunQueryStringParameters parameters #region ISAR Specific methods - public async Task ReadByIsarMissionId(string isarMissionId, bool readOnly = true) + public async Task ReadByIsarMissionId( + string isarMissionId, + bool readOnly = true + ) { return await GetMissionRunsWithSubModels(readOnly: readOnly) - .FirstOrDefaultAsync( - missionRun => - missionRun.IsarMissionId != null && missionRun.IsarMissionId.Equals(isarMissionId) + .FirstOrDefaultAsync(missionRun => + missionRun.IsarMissionId != null + && missionRun.IsarMissionId.Equals(isarMissionId) ); } - public async Task UpdateMissionRunType(string missionRunId, MissionRunType missionRunType) + public async Task UpdateMissionRunType( + string missionRunId, + MissionRunType missionRunType + ) { var missionRun = await ReadById(missionRunId, readOnly: false); if (missionRun is null) @@ -483,8 +640,10 @@ public async Task UpdateMissionRunType(string missionRunId, MissionR return await UpdateMissionRunProperty(missionRun.Id, "MissionRunType", missionRunType); } - - public async Task UpdateMissionRunStatusByIsarMissionId(string isarMissionId, MissionStatus missionStatus) + public async Task UpdateMissionRunStatusByIsarMissionId( + string isarMissionId, + MissionStatus missionStatus + ) { var missionRun = await ReadByIsarMissionId(isarMissionId, readOnly: false); if (missionRun is null) @@ -496,21 +655,37 @@ public async Task UpdateMissionRunStatusByIsarMissionId(string isarM missionRun.Status = missionStatus; - missionRun = await UpdateMissionRunProperty(missionRun.Id, "MissionStatus", missionStatus); + missionRun = await UpdateMissionRunProperty( + missionRun.Id, + "MissionStatus", + missionStatus + ); - if (missionRun.Status == MissionStatus.Failed) { _ = signalRService.SendMessageAsync("Mission run failed", missionRun?.InspectionArea?.Installation, missionRun != null ? new MissionRunResponse(missionRun) : null); } + if (missionRun.Status == MissionStatus.Failed) + { + _ = signalRService.SendMessageAsync( + "Mission run failed", + missionRun?.InspectionArea?.Installation, + missionRun != null ? new MissionRunResponse(missionRun) : null + ); + } return missionRun!; } #endregion ISAR Specific methods - public async Task UpdateMissionRunProperty(string missionRunId, string propertyName, object? value) + public async Task UpdateMissionRunProperty( + string missionRunId, + string propertyName, + object? value + ) { var missionRun = await ReadById(missionRunId, readOnly: false); if (missionRun is null) { - string errorMessage = $"Mission with ID {missionRunId} was not found in the database"; + string errorMessage = + $"Mission with ID {missionRunId} was not found in the database"; logger.LogError("{Message}", errorMessage); throw new MissionRunNotFoundException(errorMessage); } @@ -519,22 +694,42 @@ public async Task UpdateMissionRunProperty(string missionRunId, stri { if (property.Name == propertyName) { - logger.LogDebug("Setting {missionRunName} field {propertyName} from {oldValue} to {NewValue}", missionRun.Name, propertyName, property.GetValue(missionRun), value); + logger.LogDebug( + "Setting {missionRunName} field {propertyName} from {oldValue} to {NewValue}", + missionRun.Name, + propertyName, + property.GetValue(missionRun), + value + ); property.SetValue(missionRun, value); } } - try { missionRun = await Update(missionRun); } - catch (InvalidOperationException e) { logger.LogError(e, "Failed to update {missionRunName}", missionRun.Name); }; + try + { + missionRun = await Update(missionRun); + } + catch (InvalidOperationException e) + { + logger.LogError(e, "Failed to update {missionRunName}", missionRun.Name); + } + ; return missionRun; } public async Task UpdateCurrentRobotMissionToFailed(string robotId) { - var robot = await robotService.ReadById(robotId, readOnly: true) ?? throw new RobotNotFoundException($"Robot with ID: {robotId} was not found in the database"); + var robot = + await robotService.ReadById(robotId, readOnly: true) + ?? throw new RobotNotFoundException( + $"Robot with ID: {robotId} was not found in the database" + ); if (robot.CurrentMissionId != null) { - var missionRun = await SetMissionRunToFailed(robot.CurrentMissionId, "Lost connection to ISAR during mission"); + var missionRun = await SetMissionRunToFailed( + robot.CurrentMissionId, + "Lost connection to ISAR during mission" + ); logger.LogWarning( "Mission '{Id}' failed because ISAR could not be reached", missionRun.Id @@ -542,9 +737,16 @@ public async Task UpdateCurrentRobotMissionToFailed(string robotId) } } - public async Task SetMissionRunToFailed(string missionRunId, string failureDescription) + public async Task SetMissionRunToFailed( + string missionRunId, + string failureDescription + ) { - var missionRun = await ReadById(missionRunId, readOnly: false) ?? throw new MissionRunNotFoundException($"Could not find mission run with ID {missionRunId}"); + var missionRun = + await ReadById(missionRunId, readOnly: false) + ?? throw new MissionRunNotFoundException( + $"Could not find mission run with ID {missionRunId}" + ); missionRun.Status = MissionStatus.Failed; missionRun.StatusReason = failureDescription; @@ -566,14 +768,23 @@ public void DetachTracking(MissionRun missionRun) { missionTaskService.DetachTracking(task); } - if (missionRun.InspectionArea != null) deckService.DetachTracking(missionRun.InspectionArea); - if (missionRun.Robot != null) robotService.DetachTracking(missionRun.Robot); + if (missionRun.InspectionArea != null) + deckService.DetachTracking(missionRun.InspectionArea); + if (missionRun.Robot != null) + robotService.DetachTracking(missionRun.Robot); context.Entry(missionRun).State = EntityState.Detached; } - public async Task UpdateWithIsarInfo(string missionRunId, IsarMission isarMission) + public async Task UpdateWithIsarInfo( + string missionRunId, + IsarMission isarMission + ) { - var missionRun = await ReadById(missionRunId, readOnly: false) ?? throw new MissionRunNotFoundException($"Could not find mission run with ID {missionRunId}"); + var missionRun = + await ReadById(missionRunId, readOnly: false) + ?? throw new MissionRunNotFoundException( + $"Could not find mission run with ID {missionRunId}" + ); missionRun.IsarMissionId = isarMission.IsarMissionId; foreach (var isarTask in isarMission.Tasks) diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index 574870412..f0c6f05fa 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -4,6 +4,7 @@ using Api.Services.Events; using Api.Services.Models; using Api.Utilities; + namespace Api.Services { public interface IMissionSchedulingService @@ -27,90 +28,182 @@ public interface IMissionSchedulingService public void TriggerRobotAvailable(RobotAvailableEventArgs e); public Task AbortActiveReturnToHomeMission(string robotId); - } - public class MissionSchedulingService(ILogger logger, IMissionRunService missionRunService, IRobotService robotService, - IIsarService isarService, ILocalizationService localizationService, IReturnToHomeService returnToHomeService, ISignalRService signalRService, IErrorHandlingService errorHandlingService) : IMissionSchedulingService + public class MissionSchedulingService( + ILogger logger, + IMissionRunService missionRunService, + IRobotService robotService, + IIsarService isarService, + ILocalizationService localizationService, + IReturnToHomeService returnToHomeService, + ISignalRService signalRService, + IErrorHandlingService errorHandlingService + ) : IMissionSchedulingService { public async Task StartNextMissionRunIfSystemIsAvailable(Robot robot) { - logger.LogInformation("Robot {robotName} has status {robotStatus} and current area {areaName}", robot.Name, robot.Status, robot.CurrentInspectionArea?.Name); + logger.LogInformation( + "Robot {robotName} has status {robotStatus} and current area {areaName}", + robot.Name, + robot.Status, + robot.CurrentInspectionArea?.Name + ); MissionRun? missionRun; - try { missionRun = await SelectNextMissionRun(robot); } + try + { + missionRun = await SelectNextMissionRun(robot); + } catch (RobotNotFoundException) { logger.LogError("Robot with ID: {RobotId} was not found in the database", robot.Id); return; } - if (robot.MissionQueueFrozen && missionRun != null && !(missionRun.IsEmergencyMission() || missionRun.IsReturnHomeMission())) + if ( + robot.MissionQueueFrozen + && missionRun != null + && !(missionRun.IsEmergencyMission() || missionRun.IsReturnHomeMission()) + ) { - logger.LogInformation("Robot {robotName} was ready to start a mission but its mission queue was frozen", robot.Name); + logger.LogInformation( + "Robot {robotName} was ready to start a mission but its mission queue was frozen", + robot.Name + ); return; } if (missionRun == null) { - logger.LogInformation("The robot was ready to start mission, but no mission is scheduled"); + logger.LogInformation( + "The robot was ready to start mission, but no mission is scheduled" + ); - if (robot.RobotCapabilities != null && robot.RobotCapabilities.Contains(RobotCapabilitiesEnum.return_to_home)) + if ( + robot.RobotCapabilities != null + && robot.RobotCapabilities.Contains(RobotCapabilitiesEnum.return_to_home) + ) { - try { missionRun = await returnToHomeService.ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome(robot.Id); } + try + { + missionRun = + await returnToHomeService.ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome( + robot.Id + ); + } catch (ReturnToHomeMissionFailedToScheduleException) { - signalRService.ReportGeneralFailToSignalR(robot, $"Failed to schedule return to home for robot {robot.Name}", ""); - logger.LogError("Failed to schedule a return to home mission for robot {RobotId}", robot.Id); + signalRService.ReportGeneralFailToSignalR( + robot, + $"Failed to schedule return to home for robot {robot.Name}", + "" + ); + logger.LogError( + "Failed to schedule a return to home mission for robot {RobotId}", + robot.Id + ); } } } - if (missionRun == null) { return; } + if (missionRun == null) + { + return; + } if (!TheSystemIsAvailableToRunAMission(robot, missionRun)) { - logger.LogInformation("Mission {MissionRunId} was put on the queue as the system may not start a mission now", missionRun.Id); + logger.LogInformation( + "Mission {MissionRunId} was put on the queue as the system may not start a mission now", + missionRun.Id + ); return; } if (missionRun.InspectionArea == null) { - logger.LogWarning("Mission {MissionRunId} does not have an inspection area and was therefore not started", missionRun.Id); + logger.LogWarning( + "Mission {MissionRunId} does not have an inspection area and was therefore not started", + missionRun.Id + ); return; } if (robot.CurrentInspectionArea == null && missionRun.InspectionArea != null) { - await robotService.UpdateCurrentInspectionArea(robot.Id, missionRun.InspectionArea.Id); + await robotService.UpdateCurrentInspectionArea( + robot.Id, + missionRun.InspectionArea.Id + ); } - else if (!await localizationService.RobotIsOnSameDeckAsMission(robot.Id, missionRun.InspectionArea!.Id)) + else if ( + !await localizationService.RobotIsOnSameDeckAsMission( + robot.Id, + missionRun.InspectionArea!.Id + ) + ) { - logger.LogError("Robot {RobotId} is not on the same deck as the mission run {MissionRunId}. Aborting all mission runs", robot.Id, missionRun.Id); - try { await AbortAllScheduledMissions(robot.Id, "Aborted: Robot was at different deck"); } - catch (RobotNotFoundException) { logger.LogError("Failed to abort scheduled missions for robot {RobotId}", robot.Id); } + logger.LogError( + "Robot {RobotId} is not on the same deck as the mission run {MissionRunId}. Aborting all mission runs", + robot.Id, + missionRun.Id + ); + try + { + await AbortAllScheduledMissions( + robot.Id, + "Aborted: Robot was at different deck" + ); + } + catch (RobotNotFoundException) + { + logger.LogError( + "Failed to abort scheduled missions for robot {RobotId}", + robot.Id + ); + } - try { await returnToHomeService.ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome(robot.Id); } + try + { + await returnToHomeService.ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome( + robot.Id + ); + } catch (ReturnToHomeMissionFailedToScheduleException) { - logger.LogError("Failed to schedule a return to home mission for robot {RobotId}", robot.Id); + logger.LogError( + "Failed to schedule a return to home mission for robot {RobotId}", + robot.Id + ); } return; } - if ((robot.IsRobotPressureTooLow() || robot.IsRobotBatteryTooLow()) && !(missionRun.IsReturnHomeMission() || missionRun.IsEmergencyMission())) + if ( + (robot.IsRobotPressureTooLow() || robot.IsRobotBatteryTooLow()) + && !(missionRun.IsReturnHomeMission() || missionRun.IsEmergencyMission()) + ) { missionRun = await HandleBatteryAndPressureLevel(robot); - if (missionRun == null) { return; } + if (missionRun == null) + { + return; + } } - try { await StartMissionRun(missionRun, robot); } - catch (Exception ex) when ( - ex is MissionException - or RobotNotFoundException - or RobotNotAvailableException - or MissionRunNotFoundException - or IsarCommunicationException) + try + { + await StartMissionRun(missionRun, robot); + } + catch (Exception ex) + when (ex + is MissionException + or RobotNotFoundException + or RobotNotAvailableException + or MissionRunNotFoundException + or IsarCommunicationException + ) { logger.LogError( ex, @@ -118,34 +211,67 @@ or MissionRunNotFoundException missionRun.Id, ex.Message ); - await missionRunService.SetMissionRunToFailed(missionRun.Id, $"Mission run '{missionRun.Id}' was not started successfully. '{ex.Message}'"); - } - catch (RobotBusyException) - { + await missionRunService.SetMissionRunToFailed( + missionRun.Id, + $"Mission run '{missionRun.Id}' was not started successfully. '{ex.Message}'" + ); } + catch (RobotBusyException) { } } public async Task HandleBatteryAndPressureLevel(Robot robot) { if (robot.IsRobotPressureTooLow()) { - logger.LogError("Robot with ID: {RobotId} cannot start missions because pressure value is too low.", robot.Id); - signalRService.ReportGeneralFailToSignalR(robot, $"Low pressure value for robot {robot.Name}", "Pressure value is too low to start a mission."); + logger.LogError( + "Robot with ID: {RobotId} cannot start missions because pressure value is too low.", + robot.Id + ); + signalRService.ReportGeneralFailToSignalR( + robot, + $"Low pressure value for robot {robot.Name}", + "Pressure value is too low to start a mission." + ); } if (robot.IsRobotBatteryTooLow()) { - logger.LogError("Robot with ID: {RobotId} cannot start missions because battery value is too low.", robot.Id); - signalRService.ReportGeneralFailToSignalR(robot, $"Low battery value for robot {robot.Name}", "Battery value is too low to start a mission."); + logger.LogError( + "Robot with ID: {RobotId} cannot start missions because battery value is too low.", + robot.Id + ); + signalRService.ReportGeneralFailToSignalR( + robot, + $"Low battery value for robot {robot.Name}", + "Battery value is too low to start a mission." + ); } - try { await AbortAllScheduledMissions(robot.Id, "Aborted: Robot pressure or battery values are too low."); } - catch (RobotNotFoundException) { logger.LogError("Failed to abort scheduled missions for robot {RobotId}", robot.Id); } + try + { + await AbortAllScheduledMissions( + robot.Id, + "Aborted: Robot pressure or battery values are too low." + ); + } + catch (RobotNotFoundException) + { + logger.LogError("Failed to abort scheduled missions for robot {RobotId}", robot.Id); + } MissionRun? missionRun; - try { missionRun = await returnToHomeService.ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome(robot.Id); } + try + { + missionRun = + await returnToHomeService.ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome( + robot.Id + ); + } catch (ReturnToHomeMissionFailedToScheduleException) { - logger.LogError("Failed to schedule a return to home mission for robot {RobotId}", robot.Id); + logger.LogError( + "Failed to schedule a return to home mission for robot {RobotId}", + robot.Id + ); return null; } return missionRun; @@ -157,7 +283,6 @@ public async Task OngoingMission(string robotId) return ongoingMissions is not null && ongoingMissions.Any(); } - public async Task FreezeMissionRunQueueForRobot(string robotId) { await robotService.UpdateMissionQueueFrozen(robotId, true); @@ -167,7 +292,10 @@ public async Task FreezeMissionRunQueueForRobot(string robotId) public async Task UnfreezeMissionRunQueueForRobot(string robotId) { await robotService.UpdateMissionQueueFrozen(robotId, false); - logger.LogInformation("Mission queue for robot with ID {RobotId} was unfrozen", robotId); + logger.LogInformation( + "Mission queue for robot with ID {RobotId} was unfrozen", + robotId + ); } public async Task StopCurrentMissionRun(string robotId) @@ -183,14 +311,20 @@ public async Task StopCurrentMissionRun(string robotId) var ongoingMissionRuns = await GetOngoingMissions(robotId, readOnly: true); if (ongoingMissionRuns is null) { - string errorMessage = $"There were no ongoing mission runs to stop for robot {robotId}"; + string errorMessage = + $"There were no ongoing mission runs to stop for robot {robotId}"; logger.LogWarning("{Message}", errorMessage); throw new MissionRunNotFoundException(errorMessage); } - IList ongoingMissionRunIds = ongoingMissionRuns.Select(missionRun => missionRun.Id).ToList(); + IList ongoingMissionRunIds = ongoingMissionRuns + .Select(missionRun => missionRun.Id) + .ToList(); - try { await isarService.StopMission(robot); } + try + { + await isarService.StopMission(robot); + } catch (HttpRequestException e) { const string Message = "Error connecting to ISAR while stopping mission"; @@ -210,11 +344,17 @@ public async Task StopCurrentMissionRun(string robotId) logger.LogError(e, "{Message}", Message); throw new MissionException(Message, 0); } - catch (MissionNotFoundException) { logger.LogWarning("{Message}", $"No mission was running for robot {robot.Id}"); } + catch (MissionNotFoundException) + { + logger.LogWarning("{Message}", $"No mission was running for robot {robot.Id}"); + } await MoveInterruptedMissionsToQueue(ongoingMissionRunIds); - try { await robotService.UpdateCurrentMissionId(robotId, null); } + try + { + await robotService.UpdateCurrentMissionId(robotId, null); + } catch (RobotNotFoundException) { } } @@ -228,20 +368,34 @@ public async Task AbortAllScheduledMissions(string robotId, string? abortReason) throw new RobotNotFoundException(errorMessage); } - var pendingMissionRuns = await missionRunService.ReadMissionRunQueue(robotId, readOnly: true); + var pendingMissionRuns = await missionRunService.ReadMissionRunQueue( + robotId, + readOnly: true + ); if (pendingMissionRuns is null) { - string infoMessage = $"There were no mission runs in the queue to abort for robot {robotId}"; + string infoMessage = + $"There were no mission runs in the queue to abort for robot {robotId}"; logger.LogWarning("{Message}", infoMessage); return; } - IList pendingMissionRunIds = pendingMissionRuns.Select(missionRun => missionRun.Id).ToList(); + IList pendingMissionRunIds = pendingMissionRuns + .Select(missionRun => missionRun.Id) + .ToList(); foreach (var pendingMissionRun in pendingMissionRuns) { - await missionRunService.UpdateMissionRunProperty(pendingMissionRun.Id, "Status", MissionStatus.Aborted); - await missionRunService.UpdateMissionRunProperty(pendingMissionRun.Id, "StatusReason", abortReason); + await missionRunService.UpdateMissionRunProperty( + pendingMissionRun.Id, + "Status", + MissionStatus.Aborted + ); + await missionRunService.UpdateMissionRunProperty( + pendingMissionRun.Id, + "StatusReason", + abortReason + ); } } @@ -269,18 +423,15 @@ public async Task ScheduleMissionToDriveToDockPosition(string robotId) } else { - string errorMessage = $"Robot with ID {robotId} could return home as it did not have an inspection area"; + string errorMessage = + $"Robot with ID {robotId} could return home as it did not have an inspection area"; logger.LogError("{Message}", errorMessage); throw new DeckNotFoundException(errorMessage); } // Cloning to avoid tracking same object var clonedPose = ObjectCopier.Clone(robotPose); - var customTaskQuery = new CustomTaskQuery - { - RobotPose = clonedPose, - TaskOrder = 0 - }; + var customTaskQuery = new CustomTaskQuery { RobotPose = clonedPose, TaskOrder = 0 }; var missionRun = new MissionRun { @@ -291,10 +442,7 @@ public async Task ScheduleMissionToDriveToDockPosition(string robotId) InspectionArea = robot.CurrentInspectionArea!, Status = MissionStatus.Pending, DesiredStartTime = DateTime.UtcNow, - Tasks = new List( - [ - new MissionTask(customTaskQuery) - ]) + Tasks = new List([new MissionTask(customTaskQuery)]), }; try @@ -303,7 +451,9 @@ public async Task ScheduleMissionToDriveToDockPosition(string robotId) } catch (UnsupportedRobotCapabilityException) { - logger.LogError($"Unsupported robot capability detected when driving to dock for robot {missionRun.Robot.Name}. This should not happen."); + logger.LogError( + $"Unsupported robot capability detected when driving to dock for robot {missionRun.Robot.Name}. This should not happen." + ); } } @@ -319,35 +469,57 @@ public void TriggerRobotAvailable(RobotAvailableEventArgs e) private async Task SelectNextMissionRun(Robot robot) { - var missionRun = await missionRunService.ReadNextScheduledEmergencyMissionRun(robot.Id, readOnly: true); - if (robot.MissionQueueFrozen == false && missionRun == null) { missionRun = await missionRunService.ReadNextScheduledMissionRun(robot.Id, readOnly: true); } + var missionRun = await missionRunService.ReadNextScheduledEmergencyMissionRun( + robot.Id, + readOnly: true + ); + if (robot.MissionQueueFrozen == false && missionRun == null) + { + missionRun = await missionRunService.ReadNextScheduledMissionRun( + robot.Id, + readOnly: true + ); + } return missionRun; } - private async Task MoveInterruptedMissionsToQueue(IEnumerable interruptedMissionRunIds) + private async Task MoveInterruptedMissionsToQueue( + IEnumerable interruptedMissionRunIds + ) { foreach (string missionRunId in interruptedMissionRunIds) { var missionRun = await missionRunService.ReadById(missionRunId, readOnly: true); if (missionRun is null) { - logger.LogWarning("{Message}", $"Interrupted mission run with Id {missionRunId} could not be found"); + logger.LogWarning( + "{Message}", + $"Interrupted mission run with Id {missionRunId} could not be found" + ); continue; } if (missionRun.IsReturnHomeMission()) { - logger.LogWarning("Return to home mission will not be added back to the queue."); + logger.LogWarning( + "Return to home mission will not be added back to the queue." + ); return; } - var unfinishedTasks = missionRun.Tasks - .Where(t => !new List - {Database.Models.TaskStatus.Successful, Database.Models.TaskStatus.Failed} - .Contains(t.Status)) - .Select(t => new MissionTask(t)).ToList(); - - if (unfinishedTasks.Count == 0) continue; + var unfinishedTasks = missionRun + .Tasks.Where(t => + !new List + { + Database.Models.TaskStatus.Successful, + Database.Models.TaskStatus.Failed, + }.Contains(t.Status) + ) + .Select(t => new MissionTask(t)) + .ToList(); + + if (unfinishedTasks.Count == 0) + continue; var newMissionRun = new MissionRun { @@ -358,16 +530,21 @@ private async Task MoveInterruptedMissionsToQueue(IEnumerable interrupte InspectionArea = missionRun.InspectionArea, Status = MissionStatus.Pending, DesiredStartTime = DateTime.UtcNow, - Tasks = unfinishedTasks + Tasks = unfinishedTasks, }; try { - await missionRunService.Create(newMissionRun, triggerCreatedMissionRunEvent: false); + await missionRunService.Create( + newMissionRun, + triggerCreatedMissionRunEvent: false + ); } catch (UnsupportedRobotCapabilityException) { - logger.LogError($"Unsupported robot capability detected when restarting interrupted missions for robot {missionRun.Robot.Name}. This should not happen."); + logger.LogError( + $"Unsupported robot capability detected when restarting interrupted missions for robot {missionRun.Robot.Name}. This should not happen." + ); } } } @@ -385,7 +562,10 @@ private async Task StartMissionRun(MissionRun queuedMissionRun, Robot robot) } IsarMission isarMission; - try { isarMission = await isarService.StartMission(robot, missionRun); } + try + { + isarMission = await isarService.StartMission(robot, missionRun); + } catch (HttpRequestException e) { string errorMessage = $"Could not reach ISAR at {robot.IsarUri}"; @@ -407,7 +587,11 @@ private async Task StartMissionRun(MissionRun queuedMissionRun, Robot robot) } await missionRunService.UpdateWithIsarInfo(missionRun.Id, isarMission); - await missionRunService.UpdateMissionRunProperty(missionRun.Id, "Status", MissionStatus.Ongoing); + await missionRunService.UpdateMissionRunProperty( + missionRun.Id, + "Status", + MissionStatus.Ongoing + ); robot.Status = RobotStatus.Busy; await robotService.UpdateRobotStatus(robot.Id, RobotStatus.Busy); @@ -416,7 +600,10 @@ private async Task StartMissionRun(MissionRun queuedMissionRun, Robot robot) logger.LogInformation("Started mission run '{Id}'", queuedMissionRun.Id); } - private async Task?> GetOngoingMissions(string robotId, bool readOnly = true) + private async Task?> GetOngoingMissions( + string robotId, + bool readOnly = true + ) { var ongoingMissions = await missionRunService.ReadAll( new MissionRunQueryStringParameters @@ -424,33 +611,53 @@ private async Task StartMissionRun(MissionRun queuedMissionRun, Robot robot) Statuses = [MissionStatus.Ongoing], RobotId = robotId, OrderBy = "DesiredStartTime", - PageSize = 100 - }, readOnly: readOnly); + PageSize = 100, + }, + readOnly: readOnly + ); return ongoingMissions; } private bool TheSystemIsAvailableToRunAMission(Robot robot, MissionRun missionRun) { - if (robot.MissionQueueFrozen && !(missionRun.IsEmergencyMission() || missionRun.IsReturnHomeMission())) + if ( + robot.MissionQueueFrozen + && !(missionRun.IsEmergencyMission() || missionRun.IsReturnHomeMission()) + ) { - logger.LogInformation("Mission run {MissionRunId} was not started as the mission run queue for robot {RobotName} is frozen", missionRun.Id, robot.Name); + logger.LogInformation( + "Mission run {MissionRunId} was not started as the mission run queue for robot {RobotName} is frozen", + missionRun.Id, + robot.Name + ); return false; } if (robot.Status is not RobotStatus.Available) { - logger.LogInformation("Mission run {MissionRunId} was not started as the robot is not available", missionRun.Id); + logger.LogInformation( + "Mission run {MissionRunId} was not started as the robot is not available", + missionRun.Id + ); return false; } if (!robot.IsarConnected) { - logger.LogWarning("Mission run {MissionRunId} was not started as the robots {RobotId} isar instance is disconnected", missionRun.Id, robot.Id); + logger.LogWarning( + "Mission run {MissionRunId} was not started as the robots {RobotId} isar instance is disconnected", + missionRun.Id, + robot.Id + ); return false; } if (robot.Deprecated) { - logger.LogWarning("Mission run {MissionRunId} was not started as the robot {RobotId} is deprecated", missionRun.Id, robot.Id); + logger.LogWarning( + "Mission run {MissionRunId} was not started as the robot {RobotId} is deprecated", + missionRun.Id, + robot.Id + ); return false; } return true; @@ -458,25 +665,55 @@ private bool TheSystemIsAvailableToRunAMission(Robot robot, MissionRun missionRu public async Task AbortActiveReturnToHomeMission(string robotId) { - var activeReturnToHomeMission = await returnToHomeService.GetActiveReturnToHomeMissionRun(robotId, readOnly: true); + var activeReturnToHomeMission = + await returnToHomeService.GetActiveReturnToHomeMissionRun(robotId, readOnly: true); if (activeReturnToHomeMission == null) { - logger.LogWarning("Attempted to abort active Return to Home mission for robot with Id {RobotId} but none was found", robotId); + logger.LogWarning( + "Attempted to abort active Return to Home mission for robot with Id {RobotId} but none was found", + robotId + ); return; } - try { await missionRunService.UpdateMissionRunProperty(activeReturnToHomeMission.Id, "Status", MissionStatus.Aborted); } - catch (MissionRunNotFoundException) { return; } + try + { + await missionRunService.UpdateMissionRunProperty( + activeReturnToHomeMission.Id, + "Status", + MissionStatus.Aborted + ); + } + catch (MissionRunNotFoundException) + { + return; + } + + if (activeReturnToHomeMission.Status == MissionStatus.Pending) + { + return; + } - if (activeReturnToHomeMission.Status == MissionStatus.Pending) { return; } + try + { + await StopCurrentMissionRun(activeReturnToHomeMission.Robot.Id); + } + catch (RobotNotFoundException) + { + return; + } + catch (MissionRunNotFoundException) + { + return; + } + } - try { await StopCurrentMissionRun(activeReturnToHomeMission.Robot.Id); } - catch (RobotNotFoundException) { return; } - catch (MissionRunNotFoundException) { return; } + protected virtual void OnRobotAvailable(RobotAvailableEventArgs e) + { + RobotAvailable?.Invoke(this, e); } - protected virtual void OnRobotAvailable(RobotAvailableEventArgs e) { RobotAvailable?.Invoke(this, e); } public static event EventHandler? RobotAvailable; } } diff --git a/backend/api/Services/MissionTaskService.cs b/backend/api/Services/MissionTaskService.cs index 4421d0bcd..72ee2c75e 100644 --- a/backend/api/Services/MissionTaskService.cs +++ b/backend/api/Services/MissionTaskService.cs @@ -4,11 +4,15 @@ using Api.Services.Models; using Api.Utilities; using Microsoft.EntityFrameworkCore; + namespace Api.Services { public interface IMissionTaskService { - public Task UpdateMissionTaskStatus(string isarTaskId, IsarTaskStatus isarTaskStatus); + public Task UpdateMissionTaskStatus( + string isarTaskId, + IsarTaskStatus isarTaskStatus + ); public void DetachTracking(MissionTask missionTask); } @@ -18,9 +22,13 @@ public interface IMissionTaskService "CA1309:Use ordinal StringComparison", Justification = "EF Core refrains from translating string comparison overloads to SQL" )] - public class MissionTaskService(FlotillaDbContext context, ILogger logger) : IMissionTaskService + public class MissionTaskService(FlotillaDbContext context, ILogger logger) + : IMissionTaskService { - public async Task UpdateMissionTaskStatus(string isarTaskId, IsarTaskStatus isarTaskStatus) + public async Task UpdateMissionTaskStatus( + string isarTaskId, + IsarTaskStatus isarTaskStatus + ) { var missionTask = await ReadByIsarTaskId(isarTaskId, readOnly: false); if (missionTask is null) @@ -36,8 +44,8 @@ public async Task UpdateMissionTaskStatus(string isarTaskId, IsarTa private async Task Update(MissionTask missionTask) { - if (missionTask.Inspection != null) context.Entry(missionTask.Inspection).State = EntityState.Unchanged; - + if (missionTask.Inspection != null) + context.Entry(missionTask.Inspection).State = EntityState.Unchanged; var entry = context.Update(missionTask); await context.SaveChangesAsync(); @@ -46,13 +54,21 @@ private async Task Update(MissionTask missionTask) private async Task ReadByIsarTaskId(string id, bool readOnly = true) { - return await GetMissionTasks(readOnly: readOnly).FirstOrDefaultAsync(missionTask => missionTask.IsarTaskId != null && missionTask.IsarTaskId.Equals(id)); + return await GetMissionTasks(readOnly: readOnly) + .FirstOrDefaultAsync(missionTask => + missionTask.IsarTaskId != null && missionTask.IsarTaskId.Equals(id) + ); } private IQueryable GetMissionTasks(bool readOnly = true) { - return (readOnly ? context.MissionTasks.AsNoTracking() : context.MissionTasks.AsTracking()) - .Include(missionTask => missionTask.Inspection).ThenInclude(inspection => inspection != null ? inspection.InspectionFindings : null); + return ( + readOnly ? context.MissionTasks.AsNoTracking() : context.MissionTasks.AsTracking() + ) + .Include(missionTask => missionTask.Inspection) + .ThenInclude(inspection => + inspection != null ? inspection.InspectionFindings : null + ); } public void DetachTracking(MissionTask missionTask) diff --git a/backend/api/Services/Models/AlertResponse.cs b/backend/api/Services/Models/AlertResponse.cs index 4eaa9ab83..ae484b0a4 100644 --- a/backend/api/Services/Models/AlertResponse.cs +++ b/backend/api/Services/Models/AlertResponse.cs @@ -1,8 +1,15 @@ using System.Text.Json.Serialization; + namespace Api.Services.Models { [method: JsonConstructor] - public class AlertResponse(string code, string title, string message, string installationCode, string? robotId) + public class AlertResponse( + string code, + string title, + string message, + string installationCode, + string? robotId + ) { public string AlertCode { get; set; } = code; public string AlertTitle { get; set; } = title; diff --git a/backend/api/Services/Models/EnuPosition.cs b/backend/api/Services/Models/EnuPosition.cs index c7a597177..1ffd5e9a9 100644 --- a/backend/api/Services/Models/EnuPosition.cs +++ b/backend/api/Services/Models/EnuPosition.cs @@ -1,14 +1,17 @@ #nullable disable using System.Text.Json.Serialization; using Api.Database.Models; + namespace Api.Services.Models { public class EnuPosition(float east, float north, float up) { [JsonPropertyName("e")] public float East { get; } = east; + [JsonPropertyName("n")] public float North { get; } = north; + [JsonPropertyName("u")] public float Up { get; } = up; diff --git a/backend/api/Services/Models/IDAInspectionDataResponse.cs b/backend/api/Services/Models/IDAInspectionDataResponse.cs index e33c21cb5..70affa8aa 100644 --- a/backend/api/Services/Models/IDAInspectionDataResponse.cs +++ b/backend/api/Services/Models/IDAInspectionDataResponse.cs @@ -12,6 +12,5 @@ public class IDAInspectionDataResponse [JsonPropertyName("blobName")] public required string BlobName { get; set; } - } } diff --git a/backend/api/Services/Models/IsarMediaConfig.cs b/backend/api/Services/Models/IsarMediaConfig.cs index 07f3d3a38..6a9421a74 100644 --- a/backend/api/Services/Models/IsarMediaConfig.cs +++ b/backend/api/Services/Models/IsarMediaConfig.cs @@ -13,6 +13,5 @@ public class IsarMediaConfigMessage [JsonPropertyName("media_connection_type")] public string MediaConnectionType { get; set; } - } } diff --git a/backend/api/Services/Models/IsarMission.cs b/backend/api/Services/Models/IsarMission.cs index 84211b63c..5337e6d02 100644 --- a/backend/api/Services/Models/IsarMission.cs +++ b/backend/api/Services/Models/IsarMission.cs @@ -4,6 +4,7 @@ public class IsarMission(IsarStartMissionResponse missionResponse) { public string IsarMissionId { get; } = missionResponse.MissionId; - public List Tasks { get; } = missionResponse.Tasks.Select(task => new IsarTask(task)).ToList(); + public List Tasks { get; } = + missionResponse.Tasks.Select(task => new IsarTask(task)).ToList(); } } diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index ed22db36e..04dbf73d2 100644 --- a/backend/api/Services/Models/IsarMissionDefinition.cs +++ b/backend/api/Services/Models/IsarMissionDefinition.cs @@ -3,6 +3,7 @@ using System.Text.Json.Serialization; using Api.Database.Models; using Microsoft.EntityFrameworkCore; + namespace Api.Services.Models { /// @@ -31,15 +32,30 @@ public IsarMissionDefinition(List tasks) Tasks = tasks; } - public IsarMissionDefinition(MissionRun missionRun, bool includeStartPose = false, string? mapName = null) + public IsarMissionDefinition( + MissionRun missionRun, + bool includeStartPose = false, + string? mapName = null + ) { Name = missionRun.Name; - Tasks = missionRun.Tasks.Select(task => new IsarTaskDefinition(task, missionRun, mapName)).ToList(); + Tasks = missionRun + .Tasks.Select(task => new IsarTaskDefinition(task, missionRun, mapName)) + .ToList(); if (missionRun.InspectionArea != null) { - StartPose = includeStartPose && missionRun.InspectionArea.DefaultLocalizationPose != null ? new IsarPose(missionRun.InspectionArea.DefaultLocalizationPose.Pose) : null; - Undock = includeStartPose && missionRun.InspectionArea.DefaultLocalizationPose != null && missionRun.InspectionArea.DefaultLocalizationPose.DockingEnabled; - Dock = missionRun.InspectionArea.DefaultLocalizationPose != null && missionRun.InspectionArea.DefaultLocalizationPose.DockingEnabled && missionRun.IsReturnHomeMission(); + StartPose = + includeStartPose && missionRun.InspectionArea.DefaultLocalizationPose != null + ? new IsarPose(missionRun.InspectionArea.DefaultLocalizationPose.Pose) + : null; + Undock = + includeStartPose + && missionRun.InspectionArea.DefaultLocalizationPose != null + && missionRun.InspectionArea.DefaultLocalizationPose.DockingEnabled; + Dock = + missionRun.InspectionArea.DefaultLocalizationPose != null + && missionRun.InspectionArea.DefaultLocalizationPose.DockingEnabled + && missionRun.IsReturnHomeMission(); } } } @@ -64,7 +80,11 @@ public struct IsarTaskDefinition [JsonPropertyName("zoom")] public IsarZoomDescription? Zoom { get; set; } - public IsarTaskDefinition(MissionTask missionTask, MissionRun missionRun, string? mapName = null) + public IsarTaskDefinition( + MissionTask missionTask, + MissionRun missionRun, + string? mapName = null + ) { Id = missionTask.IsarTaskId; Type = MissionTask.ConvertMissionTaskTypeToIsarTaskType(missionTask.Type); @@ -72,7 +92,12 @@ public IsarTaskDefinition(MissionTask missionTask, MissionRun missionRun, string Tag = missionTask.TagId; Zoom = missionTask.IsarZoomDescription; - if (missionTask.Inspection != null) Inspection = new IsarInspectionDefinition(missionTask.Inspection, missionRun, mapName); + if (missionTask.Inspection != null) + Inspection = new IsarInspectionDefinition( + missionTask.Inspection, + missionRun, + mapName + ); } } @@ -90,25 +115,35 @@ public struct IsarInspectionDefinition [JsonPropertyName("metadata")] public Dictionary? Metadata { get; set; } - public IsarInspectionDefinition(Inspection inspection, MissionRun missionRun, string? mapName = null) + public IsarInspectionDefinition( + Inspection inspection, + MissionRun missionRun, + string? mapName = null + ) { Type = inspection.InspectionType.ToString(); - InspectionTarget = inspection.InspectionTarget != null ? new IsarPosition( - inspection.InspectionTarget.X, - inspection.InspectionTarget.Y, - inspection.InspectionTarget.Z, - "asset" - ) : null; + InspectionTarget = + inspection.InspectionTarget != null + ? new IsarPosition( + inspection.InspectionTarget.X, + inspection.InspectionTarget.Y, + inspection.InspectionTarget.Z, + "asset" + ) + : null; Duration = inspection.VideoDuration; Metadata = new Dictionary { { "map", mapName }, { "description", missionRun.Description }, - { "estimated_duration", missionRun.EstimatedDuration?.ToString("D", CultureInfo.InvariantCulture) }, + { + "estimated_duration", + missionRun.EstimatedDuration?.ToString("D", CultureInfo.InvariantCulture) + }, { "asset_code", missionRun.InstallationCode }, { "mission_name", missionRun.Name }, { "status_reason", missionRun.StatusReason }, - { "analysis_type", inspection.AnalysisType?.ToString() } + { "analysis_type", inspection.AnalysisType?.ToString() }, }; } } @@ -149,10 +184,12 @@ public readonly struct IsarPosition(float x, float y, float z, string frameName) public readonly struct IsarPose(Pose pose) { [JsonPropertyName("position")] - public IsarPosition Position { get; } = new IsarPosition(pose.Position.X, pose.Position.Y, pose.Position.Z, "asset"); + public IsarPosition Position { get; } = + new IsarPosition(pose.Position.X, pose.Position.Y, pose.Position.Z, "asset"); [JsonPropertyName("orientation")] - public IsarOrientation Orientation { get; } = new IsarOrientation( + public IsarOrientation Orientation { get; } = + new IsarOrientation( pose.Orientation.X, pose.Orientation.Y, pose.Orientation.Z, diff --git a/backend/api/Services/Models/IsarStartMissionResponse.cs b/backend/api/Services/Models/IsarStartMissionResponse.cs index 16dbae617..f1c2da1f2 100644 --- a/backend/api/Services/Models/IsarStartMissionResponse.cs +++ b/backend/api/Services/Models/IsarStartMissionResponse.cs @@ -1,6 +1,5 @@ using System.Text.Json.Serialization; - namespace Api.Services.Models { public class IsarStartMissionResponse @@ -25,7 +24,5 @@ public class IsarTaskResponse [JsonPropertyName("type")] public required string TaskType { get; set; } - - } } diff --git a/backend/api/Services/Models/IsarTask.cs b/backend/api/Services/Models/IsarTask.cs index 189664341..456e0043a 100644 --- a/backend/api/Services/Models/IsarTask.cs +++ b/backend/api/Services/Models/IsarTask.cs @@ -21,10 +21,9 @@ public static IsarTaskStatus StatusFromString(string status) "failed" => IsarTaskStatus.Failed, "cancelled" => IsarTaskStatus.Cancelled, "paused" => IsarTaskStatus.Paused, - _ - => throw new ArgumentException( - $"Failed to parse task status '{status}' - not supported" - ) + _ => throw new ArgumentException( + $"Failed to parse task status '{status}' - not supported" + ), }; } @@ -39,15 +38,13 @@ public static IsarTaskType TaskTypeFromString(string isarClassName) "take_thermal_video" => IsarTaskType.TakeThermalVideo, "return_to_home" => IsarTaskType.ReturnToHome, "move_arm" => IsarTaskType.MoveArm, - _ - => throw new ArgumentException( - $"Failed to parse step type '{isarClassName}' - not supported" - ) + _ => throw new ArgumentException( + $"Failed to parse step type '{isarClassName}' - not supported" + ), }; } } - public enum IsarTaskStatus { Successful, @@ -67,6 +64,6 @@ public enum IsarTaskType TakeThermalImage, TakeThermalVideo, RecordAudio, - MoveArm + MoveArm, } } diff --git a/backend/api/Services/Models/MediaConfig.cs b/backend/api/Services/Models/MediaConfig.cs index 08804897b..6b8153955 100644 --- a/backend/api/Services/Models/MediaConfig.cs +++ b/backend/api/Services/Models/MediaConfig.cs @@ -8,5 +8,8 @@ public struct MediaConfig public MediaConnectionType MediaConnectionType { get; set; } } - public enum MediaConnectionType { LiveKit }; + public enum MediaConnectionType + { + LiveKit, + }; } diff --git a/backend/api/Services/Models/StidTagAreaResponse.cs b/backend/api/Services/Models/StidTagAreaResponse.cs index 76ee391fb..139641f61 100644 --- a/backend/api/Services/Models/StidTagAreaResponse.cs +++ b/backend/api/Services/Models/StidTagAreaResponse.cs @@ -6,6 +6,5 @@ public class StidTagAreaResponse { [JsonPropertyName("locationCode")] public string? LocationCode { get; set; } - } } diff --git a/backend/api/Services/PlantService.cs b/backend/api/Services/PlantService.cs index 14b0163e8..2c4698878 100644 --- a/backend/api/Services/PlantService.cs +++ b/backend/api/Services/PlantService.cs @@ -5,6 +5,7 @@ using Api.Database.Models; using Api.Utilities; using Microsoft.EntityFrameworkCore; + namespace Api.Services { public interface IPlantService @@ -13,13 +14,24 @@ public interface IPlantService public Task ReadById(string id, bool readOnly = true); - public Task> ReadByInstallation(string installationCode, bool readOnly = true); + public Task> ReadByInstallation( + string installationCode, + bool readOnly = true + ); public Task ReadByPlantCode(string plantCode, bool readOnly = true); - public Task ReadByInstallationAndPlantCode(Installation installation, string plantCode, bool readOnly = true); + public Task ReadByInstallationAndPlantCode( + Installation installation, + string plantCode, + bool readOnly = true + ); - public Task ReadByInstallationAndPlantCode(string installationCode, string plantCode, bool readOnly = true); + public Task ReadByInstallationAndPlantCode( + string installationCode, + string plantCode, + bool readOnly = true + ); public Task Create(CreatePlantQuery newPlant); @@ -40,7 +52,11 @@ public interface IPlantService "CA1304:Specify CultureInfo", Justification = "Entity framework does not support translating culture info to SQL calls" )] - public class PlantService(FlotillaDbContext context, IInstallationService installationService, IAccessRoleService accessRoleService) : IPlantService + public class PlantService( + FlotillaDbContext context, + IInstallationService installationService, + IAccessRoleService accessRoleService + ) : IPlantService { public async Task> ReadAll(bool readOnly = true) { @@ -49,54 +65,95 @@ public async Task> ReadAll(bool readOnly = true) public async Task ReadById(string id, bool readOnly = true) { - return await GetPlants(readOnly: readOnly) - .FirstOrDefaultAsync(a => a.Id.Equals(id)); + return await GetPlants(readOnly: readOnly).FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task> ReadByInstallation(string installationCode, bool readOnly = true) + public async Task> ReadByInstallation( + string installationCode, + bool readOnly = true + ) { - var installation = await installationService.ReadByInstallationCode(installationCode, readOnly: true); - if (installation == null) { return []; } - return await GetPlants(readOnly: readOnly).Where(a => - a.Installation != null && a.Installation.Id.Equals(installation.Id)).ToListAsync(); + var installation = await installationService.ReadByInstallationCode( + installationCode, + readOnly: true + ); + if (installation == null) + { + return []; + } + return await GetPlants(readOnly: readOnly) + .Where(a => a.Installation != null && a.Installation.Id.Equals(installation.Id)) + .ToListAsync(); } public async Task ReadByPlantCode(string plantCode, bool readOnly = true) { - return await GetPlants(readOnly: readOnly).Where(a => - a.PlantCode.ToLower().Equals(plantCode.ToLower())).FirstOrDefaultAsync(); + return await GetPlants(readOnly: readOnly) + .Where(a => a.PlantCode.ToLower().Equals(plantCode.ToLower())) + .FirstOrDefaultAsync(); } - public async Task ReadByInstallationAndPlantCode(Installation installation, string plantCode, bool readOnly = true) + public async Task ReadByInstallationAndPlantCode( + Installation installation, + string plantCode, + bool readOnly = true + ) { - return await GetPlants(readOnly: readOnly).Where(a => - a.PlantCode.ToLower().Equals(plantCode.ToLower()) && - a.Installation != null && a.Installation.Id.Equals(installation.Id)).FirstOrDefaultAsync(); + return await GetPlants(readOnly: readOnly) + .Where(a => + a.PlantCode.ToLower().Equals(plantCode.ToLower()) + && a.Installation != null + && a.Installation.Id.Equals(installation.Id) + ) + .FirstOrDefaultAsync(); } - public async Task ReadByInstallationAndPlantCode(string installationCode, string plantCode, bool readOnly = true) + public async Task ReadByInstallationAndPlantCode( + string installationCode, + string plantCode, + bool readOnly = true + ) { - var installation = await installationService.ReadByInstallationCode(installationCode, readOnly: true); - if (installation == null) { return null; } - return await GetPlants(readOnly: readOnly).Where(a => - a.Installation != null && a.Installation.Id.Equals(installation.Id) && - a.PlantCode.ToLower().Equals(plantCode.ToLower()) - ).FirstOrDefaultAsync(); + var installation = await installationService.ReadByInstallationCode( + installationCode, + readOnly: true + ); + if (installation == null) + { + return null; + } + return await GetPlants(readOnly: readOnly) + .Where(a => + a.Installation != null + && a.Installation.Id.Equals(installation.Id) + && a.PlantCode.ToLower().Equals(plantCode.ToLower()) + ) + .FirstOrDefaultAsync(); } public async Task Create(CreatePlantQuery newPlantQuery) { - var installation = await installationService.ReadByInstallationCode(newPlantQuery.InstallationCode, readOnly: true) ?? - throw new InstallationNotFoundException($"No installation with name {newPlantQuery.InstallationCode} could be found"); - - var plant = await ReadByInstallationAndPlantCode(installation, newPlantQuery.PlantCode, readOnly: true); + var installation = + await installationService.ReadByInstallationCode( + newPlantQuery.InstallationCode, + readOnly: true + ) + ?? throw new InstallationNotFoundException( + $"No installation with name {newPlantQuery.InstallationCode} could be found" + ); + + var plant = await ReadByInstallationAndPlantCode( + installation, + newPlantQuery.PlantCode, + readOnly: true + ); if (plant == null) { plant = new Plant { Name = newPlantQuery.Name, PlantCode = newPlantQuery.PlantCode, - Installation = installation + Installation = installation, }; context.Entry(plant.Installation).State = EntityState.Unchanged; await context.Plants.AddAsync(plant); @@ -115,8 +172,7 @@ public async Task Update(Plant plant) public async Task Delete(string id) { - var plant = await GetPlants() - .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + var plant = await GetPlants().FirstOrDefaultAsync(ev => ev.Id.Equals(id)); if (plant is null) { return null; @@ -131,23 +187,37 @@ public async Task Update(Plant plant) private IQueryable GetPlants(bool readOnly = true) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - var query = context.Plants.Include(i => i.Installation) - .Where((p) => accessibleInstallationCodes.Result.Contains(p.Installation.InstallationCode.ToUpper())); + var query = context + .Plants.Include(i => i.Installation) + .Where( + (p) => + accessibleInstallationCodes.Result.Contains( + p.Installation.InstallationCode.ToUpper() + ) + ); return readOnly ? query.AsNoTracking() : query.AsTracking(); } private async Task ApplyDatabaseUpdate(Installation? installation) { var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(); - if (installation == null || accessibleInstallationCodes.Contains(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture))) + if ( + installation == null + || accessibleInstallationCodes.Contains( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + ) await context.SaveChangesAsync(); else - throw new UnauthorizedAccessException($"User does not have permission to update plant in installation {installation.Name}"); + throw new UnauthorizedAccessException( + $"User does not have permission to update plant in installation {installation.Name}" + ); } public void DetachTracking(Plant plant) { - if (plant.Installation != null) installationService.DetachTracking(plant.Installation); + if (plant.Installation != null) + installationService.DetachTracking(plant.Installation); context.Entry(plant).State = EntityState.Detached; } } diff --git a/backend/api/Services/ReturnToHomeService.cs b/backend/api/Services/ReturnToHomeService.cs index 84ff68258..b99260666 100644 --- a/backend/api/Services/ReturnToHomeService.cs +++ b/backend/api/Services/ReturnToHomeService.cs @@ -1,29 +1,64 @@ using Api.Database.Models; using Api.Utilities; + namespace Api.Services { public interface IReturnToHomeService { - public Task ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome(string robotId); - public Task GetActiveReturnToHomeMissionRun(string robotId, bool readOnly = true); + public Task ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome( + string robotId + ); + public Task GetActiveReturnToHomeMissionRun( + string robotId, + bool readOnly = true + ); } - public class ReturnToHomeService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService) : IReturnToHomeService + public class ReturnToHomeService( + ILogger logger, + IRobotService robotService, + IMissionRunService missionRunService + ) : IReturnToHomeService { - public async Task ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome(string robotId) + public async Task ScheduleReturnToHomeMissionRunIfNotAlreadyScheduledOrRobotIsHome( + string robotId + ) { - logger.LogInformation("Scheduling return to home mission if not already scheduled or the robot is home for robot {RobotId}", robotId); + logger.LogInformation( + "Scheduling return to home mission if not already scheduled or the robot is home for robot {RobotId}", + robotId + ); var lastMissionRun = await missionRunService.ReadLastExecutedMissionRunByRobot(robotId); - if (await IsReturnToHomeMissionAlreadyScheduled(robotId) || (lastMissionRun != null && (lastMissionRun.IsReturnHomeMission() || lastMissionRun.IsEmergencyMission()))) + if ( + await IsReturnToHomeMissionAlreadyScheduled(robotId) + || ( + lastMissionRun != null + && (lastMissionRun.IsReturnHomeMission() || lastMissionRun.IsEmergencyMission()) + ) + ) { - logger.LogInformation("ReturnToHomeMission is already scheduled for Robot {RobotId}", robotId); + logger.LogInformation( + "ReturnToHomeMission is already scheduled for Robot {RobotId}", + robotId + ); return null; } MissionRun missionRun; - try { missionRun = await ScheduleReturnToHomeMissionRun(robotId); } - catch (Exception ex) when (ex is RobotNotFoundException or AreaNotFoundException or DeckNotFoundException or PoseNotFoundException or UnsupportedRobotCapabilityException or MissionRunNotFoundException) + try + { + missionRun = await ScheduleReturnToHomeMissionRun(robotId); + } + catch (Exception ex) + when (ex + is RobotNotFoundException + or AreaNotFoundException + or DeckNotFoundException + or PoseNotFoundException + or UnsupportedRobotCapabilityException + or MissionRunNotFoundException + ) { // TODO: if we make ISAR aware of return to home missions, we can avoid scheduling them when the robot does not need them throw new ReturnToHomeMissionFailedToScheduleException(ex.Message); @@ -42,27 +77,41 @@ private async Task ScheduleReturnToHomeMissionRun(string robotId) var robot = await robotService.ReadById(robotId, readOnly: true); if (robot is null) { - string errorMessage = $"Robot with ID {robotId} could not be retrieved from the database"; + string errorMessage = + $"Robot with ID {robotId} could not be retrieved from the database"; logger.LogError("{Message}", errorMessage); throw new RobotNotFoundException(errorMessage); } Pose? return_to_home_pose; Deck? currentInspectionArea; - if (robot.RobotCapabilities is not null && robot.RobotCapabilities.Contains(RobotCapabilitiesEnum.auto_return_to_home)) + if ( + robot.RobotCapabilities is not null + && robot.RobotCapabilities.Contains(RobotCapabilitiesEnum.auto_return_to_home) + ) { - var previousMissionRun = await missionRunService.ReadLastExecutedMissionRunByRobot(robot.Id, readOnly: true); + var previousMissionRun = await missionRunService.ReadLastExecutedMissionRunByRobot( + robot.Id, + readOnly: true + ); currentInspectionArea = previousMissionRun?.InspectionArea; - return_to_home_pose = previousMissionRun?.InspectionArea?.DefaultLocalizationPose?.Pose == null ? new Pose() : new Pose(previousMissionRun.InspectionArea.DefaultLocalizationPose.Pose); + return_to_home_pose = + previousMissionRun?.InspectionArea?.DefaultLocalizationPose?.Pose == null + ? new Pose() + : new Pose(previousMissionRun.InspectionArea.DefaultLocalizationPose.Pose); } else { currentInspectionArea = robot.CurrentInspectionArea; - return_to_home_pose = robot.CurrentInspectionArea?.DefaultLocalizationPose?.Pose == null ? new Pose() : new Pose(robot.CurrentInspectionArea.DefaultLocalizationPose.Pose); + return_to_home_pose = + robot.CurrentInspectionArea?.DefaultLocalizationPose?.Pose == null + ? new Pose() + : new Pose(robot.CurrentInspectionArea.DefaultLocalizationPose.Pose); } if (currentInspectionArea == null) { - string errorMessage = $"Robot with ID {robotId} could return home as it did not have an inspection area"; + string errorMessage = + $"Robot with ID {robotId} could return home as it did not have an inspection area"; logger.LogError("{Message}", errorMessage); throw new DeckNotFoundException(errorMessage); } @@ -76,27 +125,47 @@ private async Task ScheduleReturnToHomeMissionRun(string robotId) InspectionArea = currentInspectionArea!, Status = MissionStatus.Pending, DesiredStartTime = DateTime.UtcNow, - Tasks = - [ - new(return_to_home_pose, MissionTaskType.ReturnHome) - ] + Tasks = [new(return_to_home_pose, MissionTaskType.ReturnHome)], }; var missionRun = await missionRunService.Create(returnToHomeMissionRun, false); logger.LogInformation( "Scheduled a mission for the robot {RobotName} to return to home location on deck {DeckName}", - robot.Name, currentInspectionArea?.Name); + robot.Name, + currentInspectionArea?.Name + ); return missionRun; } - public async Task GetActiveReturnToHomeMissionRun(string robotId, bool readOnly = true) + public async Task GetActiveReturnToHomeMissionRun( + string robotId, + bool readOnly = true + ) { - IList missionStatuses = [MissionStatus.Ongoing, MissionStatus.Pending, MissionStatus.Paused]; - var activeReturnToHomeMissions = await missionRunService.ReadMissionRuns(robotId, MissionRunType.ReturnHome, missionStatuses, readOnly: readOnly); + IList missionStatuses = + [ + MissionStatus.Ongoing, + MissionStatus.Pending, + MissionStatus.Paused, + ]; + var activeReturnToHomeMissions = await missionRunService.ReadMissionRuns( + robotId, + MissionRunType.ReturnHome, + missionStatuses, + readOnly: readOnly + ); - if (activeReturnToHomeMissions.Count == 0) { return null; } + if (activeReturnToHomeMissions.Count == 0) + { + return null; + } - if (activeReturnToHomeMissions.Count > 1) { logger.LogError($"Two Return to Home missions should not be queued or ongoing simoultaneously for robot with Id {robotId}."); } + if (activeReturnToHomeMissions.Count > 1) + { + logger.LogError( + $"Two Return to Home missions should not be queued or ongoing simoultaneously for robot with Id {robotId}." + ); + } return activeReturnToHomeMissions.FirstOrDefault(); } diff --git a/backend/api/Services/RobotModelService.cs b/backend/api/Services/RobotModelService.cs index bf8ce9d01..0cb010731 100644 --- a/backend/api/Services/RobotModelService.cs +++ b/backend/api/Services/RobotModelService.cs @@ -10,7 +10,10 @@ public interface IRobotModelService public abstract Task ReadById(string id, bool readOnly = true); - public abstract Task ReadByRobotType(RobotType robotType, bool readOnly = true); + public abstract Task ReadByRobotType( + RobotType robotType, + bool readOnly = true + ); public abstract Task Create(RobotModel newRobotModel); @@ -52,7 +55,9 @@ public async Task> ReadAll(bool readOnly = true) private IQueryable GetRobotModels(bool readOnly = true) { - return readOnly ? _context.RobotModels.AsNoTracking() : _context.RobotModels.AsTracking(); + return readOnly + ? _context.RobotModels.AsNoTracking() + : _context.RobotModels.AsTracking(); } public async Task ReadById(string id, bool readOnly = true) diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index c86077e99..41159319a 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -6,6 +6,7 @@ using Api.Database.Models; using Api.Utilities; using Microsoft.EntityFrameworkCore; + namespace Api.Services { public interface IRobotService @@ -17,7 +18,10 @@ public interface IRobotService public Task> ReadAllActivePlants(bool readOnly = true); public Task ReadById(string id, bool readOnly = true); public Task ReadByIsarId(string isarId, bool readOnly = true); - public Task> ReadRobotsForInstallation(string installationCode, bool readOnly = true); + public Task> ReadRobotsForInstallation( + string installationCode, + bool readOnly = true + ); public Task Update(Robot robot); public Task UpdateRobotStatus(string robotId, RobotStatus status); public Task UpdateRobotBatteryLevel(string robotId, float batteryLevel); @@ -46,14 +50,17 @@ public class RobotService( ISignalRService signalRService, IAccessRoleService accessRoleService, IInstallationService installationService, - IDeckService deckService) : IRobotService + IDeckService deckService + ) : IRobotService { - public async Task Create(Robot newRobot) { - if (newRobot.CurrentInstallation != null) context.Entry(newRobot.CurrentInstallation).State = EntityState.Unchanged; - if (newRobot.CurrentInspectionArea != null) context.Entry(newRobot.CurrentInspectionArea).State = EntityState.Unchanged; - if (newRobot.Model != null) context.Entry(newRobot.Model).State = EntityState.Unchanged; + if (newRobot.CurrentInstallation != null) + context.Entry(newRobot.CurrentInstallation).State = EntityState.Unchanged; + if (newRobot.CurrentInspectionArea != null) + context.Entry(newRobot.CurrentInspectionArea).State = EntityState.Unchanged; + if (newRobot.Model != null) + context.Entry(newRobot.Model).State = EntityState.Unchanged; await context.Robots.AddAsync(newRobot); await ApplyDatabaseUpdate(newRobot.CurrentInstallation); @@ -63,40 +70,70 @@ public async Task Create(Robot newRobot) public async Task CreateFromQuery(CreateRobotQuery robotQuery) { - var robotModel = await robotModelService.ReadByRobotType(robotQuery.RobotType, readOnly: true); + var robotModel = await robotModelService.ReadByRobotType( + robotQuery.RobotType, + readOnly: true + ); if (robotModel != null) { - var installation = await installationService.ReadByInstallationCode(robotQuery.CurrentInstallationCode, readOnly: true); + var installation = await installationService.ReadByInstallationCode( + robotQuery.CurrentInstallationCode, + readOnly: true + ); if (installation is null) { - logger.LogError("Installation {CurrentInstallation} does not exist", robotQuery.CurrentInstallationCode); - throw new DbUpdateException($"Could not create new robot in database as installation {robotQuery.CurrentInstallationCode} doesn't exist"); + logger.LogError( + "Installation {CurrentInstallation} does not exist", + robotQuery.CurrentInstallationCode + ); + throw new DbUpdateException( + $"Could not create new robot in database as installation {robotQuery.CurrentInstallationCode} doesn't exist" + ); } Deck? inspectionArea = null; if (robotQuery.CurrentInspectionAreaName is not null) { - inspectionArea = await deckService.ReadByInstallationAndName(robotQuery.CurrentInstallationCode, robotQuery.CurrentInspectionAreaName, readOnly: true); + inspectionArea = await deckService.ReadByInstallationAndName( + robotQuery.CurrentInstallationCode, + robotQuery.CurrentInspectionAreaName, + readOnly: true + ); if (inspectionArea is null) { - logger.LogError("Inspection area '{CurrentDeckName}' does not exist in installation {CurrentInstallation}", robotQuery.CurrentInspectionAreaName, robotQuery.CurrentInstallationCode); - throw new DbUpdateException($"Could not create new robot in database as inspection area '{robotQuery.CurrentInspectionAreaName}' does not exist in installation {robotQuery.CurrentInstallationCode}"); + logger.LogError( + "Inspection area '{CurrentDeckName}' does not exist in installation {CurrentInstallation}", + robotQuery.CurrentInspectionAreaName, + robotQuery.CurrentInstallationCode + ); + throw new DbUpdateException( + $"Could not create new robot in database as inspection area '{robotQuery.CurrentInspectionAreaName}' does not exist in installation {robotQuery.CurrentInstallationCode}" + ); } } var newRobot = new Robot(robotQuery, installation, robotModel, inspectionArea); - if (newRobot.CurrentInspectionArea is not null) context.Entry(newRobot.CurrentInspectionArea).State = EntityState.Unchanged; - if (newRobot.CurrentInstallation != null) context.Entry(newRobot.CurrentInstallation).State = EntityState.Unchanged; - if (newRobot.Model != null) context.Entry(newRobot.Model).State = EntityState.Unchanged; + if (newRobot.CurrentInspectionArea is not null) + context.Entry(newRobot.CurrentInspectionArea).State = EntityState.Unchanged; + if (newRobot.CurrentInstallation != null) + context.Entry(newRobot.CurrentInstallation).State = EntityState.Unchanged; + if (newRobot.Model != null) + context.Entry(newRobot.Model).State = EntityState.Unchanged; await context.Robots.AddAsync(newRobot); await ApplyDatabaseUpdate(newRobot.CurrentInstallation); - _ = signalRService.SendMessageAsync("Robot added", newRobot!.CurrentInstallation, new RobotResponse(newRobot!)); + _ = signalRService.SendMessageAsync( + "Robot added", + newRobot!.CurrentInstallation, + new RobotResponse(newRobot!) + ); DetachTracking(newRobot); return newRobot!; } - throw new DbUpdateException("Could not create new robot in database as robot model does not exist"); + throw new DbUpdateException( + "Could not create new robot in database as robot model does not exist" + ); } public async Task GetRobotWithPreCheck(string robotId, bool readOnly = true) @@ -119,28 +156,32 @@ public async Task GetRobotWithPreCheck(string robotId, bool readOnly = tr if (robot.IsarConnected == false) { - string errorMessage = $"The robot with ID {robotId} has connection issues. Isar not connected."; + string errorMessage = + $"The robot with ID {robotId} has connection issues. Isar not connected."; logger.LogError("{Message}", errorMessage); throw new RobotPreCheckFailedException(errorMessage); } if (robot.IsRobotPressureTooLow()) { - string errorMessage = $"The robot pressure on {robot.Name} is too low to start a mission"; + string errorMessage = + $"The robot pressure on {robot.Name} is too low to start a mission"; logger.LogError("{Message}", errorMessage); throw new RobotPreCheckFailedException(errorMessage); } if (robot.IsRobotPressureTooHigh()) { - string errorMessage = $"The robot pressure on {robot.Name} is too high to start a mission"; + string errorMessage = + $"The robot pressure on {robot.Name} is too high to start a mission"; logger.LogError("{Message}", errorMessage); throw new RobotPreCheckFailedException(errorMessage); } if (robot.IsRobotBatteryTooLow()) { - string errorMessage = $"The robot battery level on {robot.Name} is too low to start a mission"; + string errorMessage = + $"The robot battery level on {robot.Name} is too low to start a mission"; logger.LogError("{Message}", errorMessage); throw new RobotPreCheckFailedException(errorMessage); } @@ -170,7 +211,8 @@ public async Task UpdateRobotPressureLevel(string robotId, float? pressureLevel) private void ThrowIfRobotIsNull(Robot? robot, string robotId) { - if (robot is not null) return; + if (robot is not null) + return; string errorMessage = $"Robot with ID {robotId} was not found in the database"; logger.LogError("{Message}", errorMessage); @@ -179,7 +221,8 @@ private void ThrowIfRobotIsNull(Robot? robot, string robotId) public async Task UpdateRobotPose(string robotId, Pose pose) { - var robotQuery = GetRobotsWithSubModels(readOnly: true).Where(robot => robot.Id == robotId); + var robotQuery = GetRobotsWithSubModels(readOnly: true) + .Where(robot => robot.Id == robotId); var robot = await robotQuery.FirstOrDefaultAsync(); ThrowIfRobotIsNull(robot, robotId); @@ -187,15 +230,16 @@ public async Task UpdateRobotPose(string robotId, Pose pose) await robotQuery .Select(r => r.Pose) - .ExecuteUpdateAsync(poses => poses - .SetProperty(p => p.Orientation.X, pose.Orientation.X) - .SetProperty(p => p.Orientation.Y, pose.Orientation.Y) - .SetProperty(p => p.Orientation.Z, pose.Orientation.Z) - .SetProperty(p => p.Orientation.W, pose.Orientation.W) - .SetProperty(p => p.Position.X, pose.Position.X) - .SetProperty(p => p.Position.Y, pose.Position.Y) - .SetProperty(p => p.Position.Z, pose.Position.Z) - ); + .ExecuteUpdateAsync(poses => + poses + .SetProperty(p => p.Orientation.X, pose.Orientation.X) + .SetProperty(p => p.Orientation.Y, pose.Orientation.Y) + .SetProperty(p => p.Orientation.Z, pose.Orientation.Z) + .SetProperty(p => p.Orientation.W, pose.Orientation.W) + .SetProperty(p => p.Position.X, pose.Position.X) + .SetProperty(p => p.Position.Y, pose.Position.Y) + .SetProperty(p => p.Position.Z, pose.Position.Z) + ); robot = await robotQuery.FirstOrDefaultAsync(); ThrowIfRobotIsNull(robot, robotId); @@ -215,7 +259,11 @@ public async Task UpdateCurrentMissionId(string robotId, string? currentMissionI public async Task UpdateCurrentInspectionArea(string robotId, string? inspectionAreaId) { - logger.LogInformation("Updating current inspection area for robot with Id {robotId} to inspection area with Id {areaId}", robotId, inspectionAreaId); + logger.LogInformation( + "Updating current inspection area for robot with Id {robotId} to inspection area with Id {areaId}", + robotId, + inspectionAreaId + ); if (inspectionAreaId is null) { await UpdateRobotProperty(robotId, "CurrentInspectionArea", null); @@ -225,7 +273,11 @@ public async Task UpdateCurrentInspectionArea(string robotId, string? inspection var area = await deckService.ReadById(inspectionAreaId, readOnly: true); if (area is null) { - logger.LogError("Could not find inspection area '{InspectionAreaId}' setting robot '{IsarId}' inspection area to null", inspectionAreaId, robotId); + logger.LogError( + "Could not find inspection area '{InspectionAreaId}' setting robot '{IsarId}' inspection area to null", + inspectionAreaId, + robotId + ); await UpdateRobotProperty(robotId, "CurrentInspectionArea", null); } else @@ -234,18 +286,31 @@ public async Task UpdateCurrentInspectionArea(string robotId, string? inspection } } - public async Task UpdateDeprecated(string robotId, bool deprecated) { await UpdateRobotProperty(robotId, "Deprecated", deprecated); } + public async Task UpdateDeprecated(string robotId, bool deprecated) + { + await UpdateRobotProperty(robotId, "Deprecated", deprecated); + } - public async Task UpdateMissionQueueFrozen(string robotId, bool missionQueueFrozen) { await UpdateRobotProperty(robotId, "MissionQueueFrozen", missionQueueFrozen); } + public async Task UpdateMissionQueueFrozen(string robotId, bool missionQueueFrozen) + { + await UpdateRobotProperty(robotId, "MissionQueueFrozen", missionQueueFrozen); + } public async Task UpdateFlotillaStatus(string robotId, RobotFlotillaStatus status) { await UpdateRobotProperty(robotId, "FlotillaStatus", status); } - public async Task> ReadAll(bool readOnly = true) { return await GetRobotsWithSubModels(readOnly: readOnly).ToListAsync(); } + public async Task> ReadAll(bool readOnly = true) + { + return await GetRobotsWithSubModels(readOnly: readOnly).ToListAsync(); + } - public async Task ReadById(string id, bool readOnly = true) { return await GetRobotsWithSubModels(readOnly: readOnly).FirstOrDefaultAsync(robot => robot.Id.Equals(id)); } + public async Task ReadById(string id, bool readOnly = true) + { + return await GetRobotsWithSubModels(readOnly: readOnly) + .FirstOrDefaultAsync(robot => robot.Id.Equals(id)); + } public async Task ReadByIsarId(string isarId, bool readOnly = true) { @@ -255,64 +320,96 @@ public async Task UpdateFlotillaStatus(string robotId, RobotFlotillaStatus statu public async Task> ReadAllActivePlants(bool readOnly = true) { - return await GetRobotsWithSubModels(readOnly: readOnly).Where(r => r.IsarConnected && r.CurrentInstallation != null).Select(r => r.CurrentInstallation!.InstallationCode).ToListAsync(); + return await GetRobotsWithSubModels(readOnly: readOnly) + .Where(r => r.IsarConnected && r.CurrentInstallation != null) + .Select(r => r.CurrentInstallation!.InstallationCode) + .ToListAsync(); } public async Task Update(Robot robot) { - if (robot.CurrentInspectionArea is not null) context.Entry(robot.CurrentInspectionArea).State = EntityState.Unchanged; + if (robot.CurrentInspectionArea is not null) + context.Entry(robot.CurrentInspectionArea).State = EntityState.Unchanged; context.Entry(robot.Model).State = EntityState.Unchanged; context.Update(robot); await ApplyDatabaseUpdate(robot.CurrentInstallation); - _ = signalRService.SendMessageAsync("Robot updated", robot?.CurrentInstallation, robot != null ? new RobotResponse(robot) : null); + _ = signalRService.SendMessageAsync( + "Robot updated", + robot?.CurrentInstallation, + robot != null ? new RobotResponse(robot) : null + ); DetachTracking(robot!); } public async Task Delete(string id) { var robot = await GetRobotsWithSubModels().FirstOrDefaultAsync(ev => ev.Id.Equals(id)); - if (robot is null) return null; + if (robot is null) + return null; context.Robots.Remove(robot); await ApplyDatabaseUpdate(robot.CurrentInstallation); - _ = signalRService.SendMessageAsync("Robot deleted", robot?.CurrentInstallation, robot != null ? new RobotResponse(robot) : null); + _ = signalRService.SendMessageAsync( + "Robot deleted", + robot?.CurrentInstallation, + robot != null ? new RobotResponse(robot) : null + ); return robot; } - public async Task> ReadRobotsForInstallation(string installationCode, bool readOnly = true) + public async Task> ReadRobotsForInstallation( + string installationCode, + bool readOnly = true + ) { return await GetRobotsWithSubModels(readOnly: readOnly) .Where(robot => #pragma warning disable CA1304 - robot.CurrentInstallation != null && - robot.CurrentInstallation.InstallationCode.ToLower().Equals(installationCode.ToLower()) + robot.CurrentInstallation != null + && robot + .CurrentInstallation.InstallationCode.ToLower() + .Equals(installationCode.ToLower()) #pragma warning restore CA1304 - ) + ) .ToListAsync(); } private IQueryable GetRobotsWithSubModels(bool readOnly = true) { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - var query = context.Robots - .Include(r => r.Documentation) + var query = context + .Robots.Include(r => r.Documentation) .Include(r => r.Model) .Include(r => r.CurrentInstallation) .Include(r => r.CurrentInspectionArea) .ThenInclude(deck => deck != null ? deck.DefaultLocalizationPose : null) - .ThenInclude(defaultLocalizationPose => defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null) + .ThenInclude(defaultLocalizationPose => + defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null + ) .Include(r => r.CurrentInspectionArea) .ThenInclude(area => area != null ? area.Plant : null) .Include(r => r.CurrentInspectionArea) .ThenInclude(area => area != null ? area.Installation : null) #pragma warning disable CA1304 - .Where((r) => r.CurrentInstallation == null || r.CurrentInstallation.InstallationCode == null || accessibleInstallationCodes.Result.Contains(r.CurrentInstallation.InstallationCode.ToUpper())); + .Where( + (r) => + r.CurrentInstallation == null + || r.CurrentInstallation.InstallationCode == null + || accessibleInstallationCodes.Result.Contains( + r.CurrentInstallation.InstallationCode.ToUpper() + ) + ); #pragma warning restore CA1304 return readOnly ? query.AsNoTracking() : query.AsTracking(); } - private async Task UpdateRobotProperty(string robotId, string propertyName, object? value, bool isLogLevelDebug = false) + private async Task UpdateRobotProperty( + string robotId, + string propertyName, + object? value, + bool isLogLevelDebug = false + ) { var robot = await ReadById(robotId, readOnly: false); if (robot is null) @@ -327,45 +424,88 @@ private async Task UpdateRobotProperty(string robotId, string propertyName, obje if (property.Name == propertyName) { if (isLogLevelDebug) - logger.LogDebug("Setting {robotName} field {propertyName} from {oldValue} to {NewValue}", robot.Name, propertyName, property.GetValue(robot), value); + logger.LogDebug( + "Setting {robotName} field {propertyName} from {oldValue} to {NewValue}", + robot.Name, + propertyName, + property.GetValue(robot), + value + ); else - logger.LogInformation("Setting {robotName} field {propertyName} from {oldValue} to {NewValue}", robot.Name, propertyName, property.GetValue(robot), value); + logger.LogInformation( + "Setting {robotName} field {propertyName} from {oldValue} to {NewValue}", + robot.Name, + propertyName, + property.GetValue(robot), + value + ); property.SetValue(robot, value); } } - try { await Update(robot); } - catch (InvalidOperationException e) { logger.LogError(e, "Failed to update {robotName}", robot.Name); }; + try + { + await Update(robot); + } + catch (InvalidOperationException e) + { + logger.LogError(e, "Failed to update {robotName}", robot.Name); + } + ; DetachTracking(robot); } private async Task ApplyDatabaseUpdate(Installation? installation) { var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(); - if (installation == null || accessibleInstallationCodes.Contains(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture))) + if ( + installation == null + || accessibleInstallationCodes.Contains( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + ) await context.SaveChangesAsync(); else - throw new UnauthorizedAccessException($"User does not have permission to update robot in installation {installation.Name}"); + throw new UnauthorizedAccessException( + $"User does not have permission to update robot in installation {installation.Name}" + ); } - private async Task VerifyThatUserIsAuthorizedToUpdateDataForInstallation(Installation? installation) + private async Task VerifyThatUserIsAuthorizedToUpdateDataForInstallation( + Installation? installation + ) { var accessibleInstallationCodes = await accessRoleService.GetAllowedInstallationCodes(); - if (installation == null || accessibleInstallationCodes.Contains(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture))) return; + if ( + installation == null + || accessibleInstallationCodes.Contains( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + ) + return; - throw new UnauthorizedAccessException($"User does not have permission to update robot in installation {installation.Name}"); + throw new UnauthorizedAccessException( + $"User does not have permission to update robot in installation {installation.Name}" + ); } private void NotifySignalROfUpdatedRobot(Robot robot, Installation installation) { - _ = signalRService.SendMessageAsync("Robot updated", installation, robot != null ? new RobotResponse(robot) : null); + _ = signalRService.SendMessageAsync( + "Robot updated", + installation, + robot != null ? new RobotResponse(robot) : null + ); } public void DetachTracking(Robot robot) { - if (robot.CurrentInstallation != null) installationService.DetachTracking(robot.CurrentInstallation); - if (robot.CurrentInspectionArea != null) deckService.DetachTracking(robot.CurrentInspectionArea); - if (robot.Model != null) robotModelService.DetachTracking(robot.Model); + if (robot.CurrentInstallation != null) + installationService.DetachTracking(robot.CurrentInstallation); + if (robot.CurrentInspectionArea != null) + deckService.DetachTracking(robot.CurrentInspectionArea); + if (robot.Model != null) + robotModelService.DetachTracking(robot.Model); context.Entry(robot).State = EntityState.Detached; } } diff --git a/backend/api/Services/SignalRService.cs b/backend/api/Services/SignalRService.cs index 989b0ce7e..40425910c 100644 --- a/backend/api/Services/SignalRService.cs +++ b/backend/api/Services/SignalRService.cs @@ -5,6 +5,7 @@ using Api.Services.Models; using Api.SignalRHubs; using Microsoft.AspNetCore.SignalR; + namespace Api.Services { public interface ISignalRService @@ -28,7 +29,11 @@ public SignalRService(IHubContext signalRHub) _serializerOptions.Converters.Add(new JsonStringEnumConverter()); } - public async Task SendMessageAsync(string label, Installation? installation, T messageObject) + public async Task SendMessageAsync( + string label, + Installation? installation, + T messageObject + ) { string json = JsonSerializer.Serialize(messageObject, _serializerOptions); await SendMessageAsync(label, installation, json); @@ -39,22 +44,31 @@ public async Task SendMessageAsync(string label, Installation? installation, str if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Local") { string? localDevUser = Environment.GetEnvironmentVariable("LOCAL_DEVUSERID"); - if (localDevUser is null || localDevUser.Equals("", StringComparison.Ordinal)) return; + if (localDevUser is null || localDevUser.Equals("", StringComparison.Ordinal)) + return; if (installation != null) - await _signalRHub.Clients.Group(localDevUser + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture)).SendAsync(label, "all", message); + await _signalRHub + .Clients.Group( + localDevUser + + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + .SendAsync(label, "all", message); else await _signalRHub.Clients.Group(localDevUser).SendAsync(label, "all", message); } else { if (installation != null) - await _signalRHub.Clients.Group(installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture)).SendAsync(label, "all", message); + await _signalRHub + .Clients.Group( + installation.InstallationCode.ToUpper(CultureInfo.CurrentCulture) + ) + .SendAsync(label, "all", message); else await _signalRHub.Clients.All.SendAsync(label, "all", message); } - await Task.CompletedTask; } @@ -63,23 +77,44 @@ public void ReportDockFailureToSignalR(Robot robot, string message) _ = SendMessageAsync( "Alert", robot.CurrentInstallation, - new AlertResponse("DockFailure", "Dock failure", message, robot.CurrentInstallation.InstallationCode, robot.Id)); + new AlertResponse( + "DockFailure", + "Dock failure", + message, + robot.CurrentInstallation.InstallationCode, + robot.Id + ) + ); } public void ReportDockSuccessToSignalR(Robot robot, string message) { _ = SendMessageAsync( - "Alert", - robot.CurrentInstallation, - new AlertResponse("DockSuccess", "Successful drive to Dock", message, robot.CurrentInstallation.InstallationCode, robot.Id)); + "Alert", + robot.CurrentInstallation, + new AlertResponse( + "DockSuccess", + "Successful drive to Dock", + message, + robot.CurrentInstallation.InstallationCode, + robot.Id + ) + ); } public void ReportGeneralFailToSignalR(Robot robot, string title, string message) { - _ = SendMessageAsync("Alert", + _ = SendMessageAsync( + "Alert", robot.CurrentInstallation, - new AlertResponse("generalFailure", title, message, robot.CurrentInstallation.InstallationCode, robot.Id)); + new AlertResponse( + "generalFailure", + title, + message, + robot.CurrentInstallation.InstallationCode, + robot.Id + ) + ); } - } } diff --git a/backend/api/Services/SortingService.cs b/backend/api/Services/SortingService.cs index 193032579..e8db8f13a 100644 --- a/backend/api/Services/SortingService.cs +++ b/backend/api/Services/SortingService.cs @@ -7,7 +7,8 @@ namespace Api.Services { public class SortingService { - public static void ApplySort(ref IQueryable missions, string orderByQueryString) where T : SortableRecord + public static void ApplySort(ref IQueryable missions, string orderByQueryString) + where T : SortableRecord { if (string.IsNullOrWhiteSpace(orderByQueryString)) { @@ -32,18 +33,16 @@ public static void ApplySort(ref IQueryable missions, string orderByQueryS continue; string propertyFromQueryName = param.Split(" ")[0]; - var objectProperty = propertyInfos.FirstOrDefault( - pi => - pi.Name.Equals( - propertyFromQueryName, - StringComparison.Ordinal - ) - ) ?? throw new InvalidDataException( + var objectProperty = + propertyInfos.FirstOrDefault(pi => + pi.Name.Equals(propertyFromQueryName, StringComparison.Ordinal) + ) + ?? throw new InvalidDataException( $"Mission has no property '{propertyFromQueryName}' for ordering" ); string sortingOrder = param.EndsWith(" desc", StringComparison.OrdinalIgnoreCase) - ? "descending" - : "ascending"; + ? "descending" + : "ascending"; string sortParameter = $"{objectProperty.Name} {sortingOrder}, "; orderQueryBuilder.Append(sortParameter); @@ -52,8 +51,8 @@ public static void ApplySort(ref IQueryable missions, string orderByQueryS string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); missions = string.IsNullOrWhiteSpace(orderQuery) - ? missions.OrderBy(mission => mission.Name) - : missions.OrderBy(orderQuery); + ? missions.OrderBy(mission => mission.Name) + : missions.OrderBy(orderQuery); } } } diff --git a/backend/api/Services/SourceService.cs b/backend/api/Services/SourceService.cs index 2ddf31525..951b9654b 100644 --- a/backend/api/Services/SourceService.cs +++ b/backend/api/Services/SourceService.cs @@ -18,7 +18,10 @@ public interface ISourceService public abstract Task CheckForExistingSourceFromTasks(IList tasks); - public abstract Task CreateSourceIfDoesNotExist(List tasks, bool readOnly = true); + public abstract Task CreateSourceIfDoesNotExist( + List tasks, + bool readOnly = true + ); public abstract Task?> GetMissionTasksFromSourceId(string id); @@ -32,9 +35,8 @@ public interface ISourceService "CA1309:Use ordinal StringComparison", Justification = "EF Core refrains from translating string comparison overloads to SQL" )] - public class SourceService( - FlotillaDbContext context, - ILogger logger) : ISourceService + public class SourceService(FlotillaDbContext context, ILogger logger) + : ISourceService { public async Task Create(Source source) { @@ -58,8 +60,7 @@ private IQueryable GetSources(bool readOnly = true) public async Task ReadById(string id, bool readOnly = true) { - return await GetSources(readOnly: readOnly) - .FirstOrDefaultAsync(s => s.Id.Equals(id)); + return await GetSources(readOnly: readOnly).FirstOrDefaultAsync(s => s.Id.Equals(id)); } public async Task ReadBySourceId(string sourceId, bool readOnly = true) @@ -82,13 +83,17 @@ private IQueryable GetSources(bool readOnly = true) public async Task?> GetMissionTasksFromSourceId(string id) { var existingSource = await ReadBySourceId(id, readOnly: true); - if (existingSource == null || existingSource.CustomMissionTasks == null) return null; + if (existingSource == null || existingSource.CustomMissionTasks == null) + return null; try { - var content = JsonSerializer.Deserialize>(existingSource.CustomMissionTasks); + var content = JsonSerializer.Deserialize>( + existingSource.CustomMissionTasks + ); - if (content == null) return null; + if (content == null) + return null; foreach (var task in content) { @@ -99,27 +104,29 @@ private IQueryable GetSources(bool readOnly = true) } catch (Exception e) { - logger.LogWarning("Unable to deserialize custom mission tasks with ID {Id}. {ErrorMessage}", id, e); + logger.LogWarning( + "Unable to deserialize custom mission tasks with ID {Id}. {ErrorMessage}", + id, + e + ); return null; } } - public async Task CreateSourceIfDoesNotExist(List tasks, bool readOnly = true) + public async Task CreateSourceIfDoesNotExist( + List tasks, + bool readOnly = true + ) { string json = JsonSerializer.Serialize(tasks); string hash = MissionTask.CalculateHashFromTasks(tasks); var existingSource = await ReadById(hash, readOnly: readOnly); - if (existingSource != null) return existingSource; + if (existingSource != null) + return existingSource; - var newSource = await Create( - new Source - { - SourceId = hash, - CustomMissionTasks = json - } - ); + var newSource = await Create(new Source { SourceId = hash, CustomMissionTasks = json }); DetachTracking(newSource); return newSource; @@ -127,8 +134,7 @@ public async Task CreateSourceIfDoesNotExist(List tasks, bo public async Task Delete(string id) { - var source = await GetSources() - .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + var source = await GetSources().FirstOrDefaultAsync(ev => ev.Id.Equals(id)); if (source is null) { return null; diff --git a/backend/api/Services/StidService.cs b/backend/api/Services/StidService.cs index 1a9c71e15..f694c474d 100644 --- a/backend/api/Services/StidService.cs +++ b/backend/api/Services/StidService.cs @@ -11,7 +11,11 @@ public interface IStidService public abstract Task GetTagArea(string tag, string installationCode); } - public class StidService(ILogger logger, IDownstreamApi stidApi, IAreaService areaService) : IStidService + public class StidService( + ILogger logger, + IDownstreamApi stidApi, + IAreaService areaService + ) : IStidService { public const string ServiceName = "StidApi"; @@ -30,7 +34,8 @@ public class StidService(ILogger logger, IDownstreamApi stidApi, IA response.EnsureSuccessStatusCode(); var stidTagAreaResponse = - await response.Content.ReadFromJsonAsync() ?? throw new JsonException("Failed to deserialize tag position from STID"); + await response.Content.ReadFromJsonAsync() + ?? throw new JsonException("Failed to deserialize tag position from STID"); if (stidTagAreaResponse.LocationCode == null) { @@ -39,11 +44,16 @@ public class StidService(ILogger logger, IDownstreamApi stidApi, IA return null; } - var area = await areaService.ReadByInstallationAndName(installationCode, stidTagAreaResponse.LocationCode, readOnly: true); + var area = await areaService.ReadByInstallationAndName( + installationCode, + stidTagAreaResponse.LocationCode, + readOnly: true + ); if (area == null) { - string errorMessage = $"Could not find area for area name {stidTagAreaResponse.LocationCode}"; + string errorMessage = + $"Could not find area for area name {stidTagAreaResponse.LocationCode}"; logger.LogError("{Message}", errorMessage); return null; } diff --git a/backend/api/Services/TeamsMessageService.cs b/backend/api/Services/TeamsMessageService.cs index 7e570eb66..69bcfab47 100644 --- a/backend/api/Services/TeamsMessageService.cs +++ b/backend/api/Services/TeamsMessageService.cs @@ -10,7 +10,12 @@ public interface ITeamsMessageService public class TeamsMessageService() : ITeamsMessageService { public static event EventHandler? TeamsMessage; - protected virtual void OnTeamsMessageReceived(TeamsMessageEventArgs e) { TeamsMessage?.Invoke(this, e); } + + protected virtual void OnTeamsMessageReceived(TeamsMessageEventArgs e) + { + TeamsMessage?.Invoke(this, e); + } + public void TriggerTeamsMessageReceived(TeamsMessageEventArgs e) { OnTeamsMessageReceived(e); diff --git a/backend/api/Services/TimeseriesService.cs b/backend/api/Services/TimeseriesService.cs index 0c194be00..3f58f589d 100644 --- a/backend/api/Services/TimeseriesService.cs +++ b/backend/api/Services/TimeseriesService.cs @@ -6,18 +6,21 @@ using Api.Utilities; using Microsoft.EntityFrameworkCore; using Npgsql; + namespace Api.Services { public interface ITimeseriesService { public Task> ReadAll( TimeseriesQueryStringParameters queryStringParameters - ) where T : TimeseriesBase; + ) + where T : TimeseriesBase; public Task AddBatteryEntry(string currentMissionId, float batteryLevel, string robotId); public Task AddPressureEntry(string currentMissionId, float pressureLevel, string robotId); public Task AddPoseEntry(string currentMissionId, Pose robotPose, string robotId); - public Task Create(T newTimeseries) where T : TimeseriesBase; + public Task Create(T newTimeseries) + where T : TimeseriesBase; } [SuppressMessage( @@ -33,16 +36,22 @@ public class TimeseriesService : ITimeseriesService public TimeseriesService(FlotillaDbContext context, ILogger logger) { - string? connectionString = context.Database.GetConnectionString() ?? throw new NotSupportedException( - "Could not get connection string from EF core Database context - Cannot connect to Timeseries" - ); + string? connectionString = + context.Database.GetConnectionString() + ?? throw new NotSupportedException( + "Could not get connection string from EF core Database context - Cannot connect to Timeseries" + ); var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString); _dataSource = dataSourceBuilder.Build(); _logger = logger; _context = context; } - public async Task AddBatteryEntry(string currentMissionId, float batteryLevel, string robotId) + public async Task AddBatteryEntry( + string currentMissionId, + float batteryLevel, + string robotId + ) { try { @@ -52,17 +61,24 @@ await Create( MissionId = currentMissionId, BatteryLevel = batteryLevel, RobotId = robotId, - Time = DateTime.UtcNow + Time = DateTime.UtcNow, } ); } catch (NpgsqlException e) { - _logger.LogError(e, "An error occurred setting battery level while connecting to the timeseries database"); + _logger.LogError( + e, + "An error occurred setting battery level while connecting to the timeseries database" + ); } } - public async Task AddPressureEntry(string currentMissionId, float pressureLevel, string robotId) + public async Task AddPressureEntry( + string currentMissionId, + float pressureLevel, + string robotId + ) { try { @@ -72,13 +88,16 @@ await Create( MissionId = currentMissionId, Pressure = pressureLevel, RobotId = robotId, - Time = DateTime.UtcNow + Time = DateTime.UtcNow, } ); } catch (NpgsqlException e) { - _logger.LogError(e, "An error occurred setting pressure level while connecting to the timeseries database"); + _logger.LogError( + e, + "An error occurred setting pressure level while connecting to the timeseries database" + ); } } @@ -91,19 +110,23 @@ await Create( { MissionId = currentMissionId, RobotId = robotId, - Time = DateTime.UtcNow + Time = DateTime.UtcNow, } ); } catch (NpgsqlException e) { - _logger.LogError(e, "An error occurred setting pose while connecting to the timeseries database"); + _logger.LogError( + e, + "An error occurred setting pose while connecting to the timeseries database" + ); } } public async Task> ReadAll( TimeseriesQueryStringParameters queryStringParameters - ) where T : TimeseriesBase + ) + where T : TimeseriesBase { var query = _context.Set().AsQueryable(); var filter = ConstructFilter(queryStringParameters); @@ -122,7 +145,8 @@ TimeseriesQueryStringParameters queryStringParameters // https://gibinfrancis.medium.com/timescale-db-with-ef-core-94c948829608 // https://github.com/npgsql/npgsql // Unfortunately need to use npgsql framework with heavy statements for this. - public async Task Create(T newTimeseries) where T : TimeseriesBase + public async Task Create(T newTimeseries) + where T : TimeseriesBase { await using var connection = await _dataSource.OpenConnectionAsync(); @@ -180,7 +204,8 @@ public async Task Create(T newTimeseries) where T : TimeseriesBase private static Expression> ConstructFilter( TimeseriesQueryStringParameters parameters - ) where T : TimeseriesBase + ) + where T : TimeseriesBase { Expression> robotIdFilter = parameters.RobotId is null ? timeseries => true @@ -272,12 +297,14 @@ private static void AddParameterValues(NpgsqlParameterCollection parameters, } } - private static string GetColumnNames(T entity) where T : TimeseriesBase + private static string GetColumnNames(T entity) + where T : TimeseriesBase { return GetContentNames(entity, "\"", "\""); } - private static string GetValueNames(T entity) where T : TimeseriesBase + private static string GetValueNames(T entity) + where T : TimeseriesBase { return GetContentNames(entity, "@"); } @@ -286,7 +313,8 @@ private static string GetContentNames( T entity, string namePrefix = "", string namePostfix = "" - ) where T : TimeseriesBase + ) + where T : TimeseriesBase { if (entity is RobotPressureTimeseries robotPressureTimeseries) { @@ -300,8 +328,7 @@ private static string GetContentNames( if (entity is RobotPoseTimeseries robotPoseTimeseries) { - return - $"{namePrefix}{nameof(robotPoseTimeseries.PositionX)}{namePostfix},{namePrefix}{nameof(robotPoseTimeseries.PositionY)}{namePostfix},{namePrefix}{nameof(robotPoseTimeseries.PositionZ)}{namePostfix}," + return $"{namePrefix}{nameof(robotPoseTimeseries.PositionX)}{namePostfix},{namePrefix}{nameof(robotPoseTimeseries.PositionY)}{namePostfix},{namePrefix}{nameof(robotPoseTimeseries.PositionZ)}{namePostfix}," + $"{namePrefix}{nameof(robotPoseTimeseries.OrientationX)}{namePostfix},{namePrefix}{nameof(robotPoseTimeseries.OrientationY)}{namePostfix},{namePrefix}{nameof(robotPoseTimeseries.OrientationZ)}{namePostfix},{namePrefix}{nameof(robotPoseTimeseries.OrientationW)}{namePostfix},"; } diff --git a/backend/api/Services/TimeseriesServiceSqlLite.cs b/backend/api/Services/TimeseriesServiceSqlLite.cs index ce9cb27f6..99224cae5 100644 --- a/backend/api/Services/TimeseriesServiceSqlLite.cs +++ b/backend/api/Services/TimeseriesServiceSqlLite.cs @@ -4,6 +4,7 @@ using Api.Database.Context; using Api.Database.Models; using Api.Utilities; + namespace Api.Services { /// @@ -19,7 +20,8 @@ public class TimeseriesServiceSqlLite(FlotillaDbContext context) : ITimeseriesSe { public async Task> ReadAll( TimeseriesQueryStringParameters queryStringParameters - ) where T : TimeseriesBase + ) + where T : TimeseriesBase { var query = context.Set().AsQueryable(); var filter = ConstructFilter(queryStringParameters); @@ -34,13 +36,31 @@ TimeseriesQueryStringParameters queryStringParameters ); } - public async Task AddBatteryEntry(string currentMissionId, float batteryLevel, string robotId) { await Task.CompletedTask; } + public async Task AddBatteryEntry( + string currentMissionId, + float batteryLevel, + string robotId + ) + { + await Task.CompletedTask; + } - public async Task AddPressureEntry(string currentMissionId, float pressureLevel, string robotId) { await Task.CompletedTask; } + public async Task AddPressureEntry( + string currentMissionId, + float pressureLevel, + string robotId + ) + { + await Task.CompletedTask; + } - public async Task AddPoseEntry(string currentMissionId, Pose robotPose, string robotId) { await Task.CompletedTask; } + public async Task AddPoseEntry(string currentMissionId, Pose robotPose, string robotId) + { + await Task.CompletedTask; + } - public async Task Create(T newTimeseries) where T : TimeseriesBase + public async Task Create(T newTimeseries) + where T : TimeseriesBase { await Task.CompletedTask; return newTimeseries; @@ -48,7 +68,8 @@ public async Task Create(T newTimeseries) where T : TimeseriesBase private static Expression> ConstructFilter( TimeseriesQueryStringParameters parameters - ) where T : TimeseriesBase + ) + where T : TimeseriesBase { Expression> robotIdFilter = parameters.RobotId is null ? timeseries => true diff --git a/backend/api/Services/UserInfoServices.cs b/backend/api/Services/UserInfoServices.cs index fab0af33a..77559ea5a 100644 --- a/backend/api/Services/UserInfoServices.cs +++ b/backend/api/Services/UserInfoServices.cs @@ -25,7 +25,11 @@ public interface IUserInfoService "CA1309:Use ordinal StringComparison", Justification = "EF Core refrains from translating string comparison overloads to SQL" )] - public class UserInfoService(FlotillaDbContext context, IHttpContextAccessor httpContextAccessor, ILogger logger) : IUserInfoService + public class UserInfoService( + FlotillaDbContext context, + IHttpContextAccessor httpContextAccessor, + ILogger logger + ) : IUserInfoService { public async Task> ReadAll(bool readOnly = true) { @@ -39,8 +43,7 @@ private IQueryable GetUsersInfo(bool readOnly = true) public async Task ReadById(string id, bool readOnly = true) { - return await GetUsersInfo(readOnly: readOnly) - .FirstOrDefaultAsync(a => a.Id.Equals(id)); + return await GetUsersInfo(readOnly: readOnly).FirstOrDefaultAsync(a => a.Id.Equals(id)); } public async Task ReadByOid(string oid, bool readOnly = true) @@ -92,10 +95,7 @@ public async Task Update(UserInfo userInfo) var userInfo = await ReadByOid(objectId, readOnly: false); if (userInfo is null) { - var newUserInfo = new UserInfo - { - Oid = objectId - }; + var newUserInfo = new UserInfo { Oid = objectId }; userInfo = await Create(newUserInfo); } return userInfo; diff --git a/backend/api/SignalR/SignalRHub.cs b/backend/api/SignalR/SignalRHub.cs index 1c6da9477..7863edd82 100644 --- a/backend/api/SignalR/SignalRHub.cs +++ b/backend/api/SignalR/SignalRHub.cs @@ -3,9 +3,7 @@ namespace Api.SignalRHubs { - public interface ISignalRClient - { - } + public interface ISignalRClient { } public class SignalRHub(IAccessRoleService accessRoleService) : Hub { @@ -16,8 +14,12 @@ public override async Task OnConnectedAsync() { if (Context.User != null) { - var roles = Context.User.Claims - .Where((c) => c.Type.EndsWith("/role", StringComparison.CurrentCulture)).Select((c) => c.Value).ToList(); + var roles = Context + .User.Claims.Where( + (c) => c.Type.EndsWith("/role", StringComparison.CurrentCulture) + ) + .Select((c) => c.Value) + .ToList(); var installationCodes = await accessRoleService.GetAllowedInstallationCodes(roles); @@ -25,16 +27,24 @@ public override async Task OnConnectedAsync() { string? localDevUser = Environment.GetEnvironmentVariable("LOCAL_DEVUSERID"); if (localDevUser is null || localDevUser.Equals("", StringComparison.Ordinal)) - throw new HubException("Running in development mode, but missing LOCAL_DEVUSERID value in environment"); + throw new HubException( + "Running in development mode, but missing LOCAL_DEVUSERID value in environment" + ); await Groups.AddToGroupAsync(Context.ConnectionId, localDevUser); // This is used instead of Users.All foreach (string installationCode in installationCodes) - await Groups.AddToGroupAsync(Context.ConnectionId, localDevUser + installationCode.ToUpperInvariant()); + await Groups.AddToGroupAsync( + Context.ConnectionId, + localDevUser + installationCode.ToUpperInvariant() + ); } else { foreach (string installationCode in installationCodes) - await Groups.AddToGroupAsync(Context.ConnectionId, installationCode.ToUpperInvariant()); + await Groups.AddToGroupAsync( + Context.ConnectionId, + installationCode.ToUpperInvariant() + ); } } diff --git a/backend/api/Utilities/AsyncExtensions.cs b/backend/api/Utilities/AsyncExtensions.cs index d859fc131..e2d21b6fd 100644 --- a/backend/api/Utilities/AsyncExtensions.cs +++ b/backend/api/Utilities/AsyncExtensions.cs @@ -19,7 +19,9 @@ public static CancellationTokenAwaiter GetAwaiter(this CancellationToken ct) /// The awaiter for cancellation tokens. /// [EditorBrowsable(EditorBrowsableState.Never)] - public struct CancellationTokenAwaiter(CancellationToken cancellationToken) : INotifyCompletion, ICriticalNotifyCompletion + public struct CancellationTokenAwaiter(CancellationToken cancellationToken) + : INotifyCompletion, + ICriticalNotifyCompletion { internal CancellationToken _cancellationToken = cancellationToken; diff --git a/backend/api/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs index 1d929616f..07860f663 100644 --- a/backend/api/Utilities/Exceptions.cs +++ b/backend/api/Utilities/Exceptions.cs @@ -1,124 +1,74 @@ namespace Api.Utilities { - public class ConfigException(string message) : Exception(message) - { - } + public class ConfigException(string message) : Exception(message) { } public class MissionException : Exception { - public MissionException(string message) : base(message) { } + public MissionException(string message) + : base(message) { } - public MissionException(string message, int isarStatusCode) : base(message) + public MissionException(string message, int isarStatusCode) + : base(message) { IsarStatusCode = isarStatusCode; } + public int IsarStatusCode { get; set; } } - public class MissionSourceTypeException(string message) : Exception(message) - { - } + public class MissionSourceTypeException(string message) : Exception(message) { } - public class SourceException(string message) : Exception(message) - { - } + public class SourceException(string message) : Exception(message) { } - public class InstallationNotFoundException(string message) : Exception(message) - { - } + public class InstallationNotFoundException(string message) : Exception(message) { } - public class PlantNotFoundException(string message) : Exception(message) - { - } + public class PlantNotFoundException(string message) : Exception(message) { } - public class DeckNotFoundException(string message) : Exception(message) - { - } + public class DeckNotFoundException(string message) : Exception(message) { } - public class AreaNotFoundException(string message) : Exception(message) - { - } + public class AreaNotFoundException(string message) : Exception(message) { } - public class MissionNotFoundException(string message) : Exception(message) - { - } + public class MissionNotFoundException(string message) : Exception(message) { } - public class InspectionNotFoundException(string message) : Exception(message) - { - } + public class InspectionNotFoundException(string message) : Exception(message) { } - public class MissionTaskNotFoundException(string message) : Exception(message) - { - } + public class MissionTaskNotFoundException(string message) : Exception(message) { } - public class MissionRunNotFoundException(string message) : Exception(message) - { - } + public class MissionRunNotFoundException(string message) : Exception(message) { } - public class RobotPositionNotFoundException(string message) : Exception(message) - { - } + public class RobotPositionNotFoundException(string message) : Exception(message) { } - public class RobotNotFoundException(string message) : Exception(message) - { - } + public class RobotNotFoundException(string message) : Exception(message) { } - public class RobotInformationNotAvailableException(string message) : Exception(message) - { - } + public class RobotInformationNotAvailableException(string message) : Exception(message) { } - public class RobotPreCheckFailedException(string message) : Exception(message) - { - } + public class RobotPreCheckFailedException(string message) : Exception(message) { } - public class TagPositionNotFoundException(string message) : Exception(message) - { - } + public class TagPositionNotFoundException(string message) : Exception(message) { } - public class AreaExistsException(string message) : Exception(message) - { - } + public class AreaExistsException(string message) : Exception(message) { } - public class DeckExistsException(string message) : Exception(message) - { - } + public class DeckExistsException(string message) : Exception(message) { } - public class DockException(string message) : Exception(message) - { - } + public class DockException(string message) : Exception(message) { } - public class RobotNotAvailableException(string message) : Exception(message) - { - } + public class RobotNotAvailableException(string message) : Exception(message) { } - public class RobotBusyException(string message) : Exception(message) - { - } + public class RobotBusyException(string message) : Exception(message) { } - public class RobotNotInSameInstallationAsMissionException(string message) : Exception(message) - { - } + public class RobotNotInSameInstallationAsMissionException(string message) + : Exception(message) { } - public class PoseNotFoundException(string message) : Exception(message) - { - } + public class PoseNotFoundException(string message) : Exception(message) { } - public class IsarCommunicationException(string message) : Exception(message) - { - } + public class IsarCommunicationException(string message) : Exception(message) { } - public class ReturnToHomeMissionFailedToScheduleException(string message) : Exception(message) - { - } - public class RobotCurrentAreaMissingException(string message) : Exception(message) - { - } + public class ReturnToHomeMissionFailedToScheduleException(string message) + : Exception(message) { } - public class UnsupportedRobotCapabilityException(string message) : Exception(message) - { - } + public class RobotCurrentAreaMissingException(string message) : Exception(message) { } - public class DatabaseUpdateException(string message) : Exception(message) - { - } + public class UnsupportedRobotCapabilityException(string message) : Exception(message) { } + + public class DatabaseUpdateException(string message) : Exception(message) { } } diff --git a/backend/api/Utilities/HttpContextExtensions.cs b/backend/api/Utilities/HttpContextExtensions.cs index 4ecb5933f..6dc7f64a4 100644 --- a/backend/api/Utilities/HttpContextExtensions.cs +++ b/backend/api/Utilities/HttpContextExtensions.cs @@ -17,7 +17,13 @@ public static string GetRequestToken(this HttpContext client) public static List GetRequestedRoles(this HttpContext client) { var claims = client.GetRequestedClaims(); - var roles = claims.Where((c) => c.Type == "roles" || c.Type.EndsWith("role", StringComparison.CurrentCulture)).ToList(); + var roles = claims + .Where( + (c) => + c.Type == "roles" + || c.Type.EndsWith("role", StringComparison.CurrentCulture) + ) + .ToList(); return roles; } @@ -39,7 +45,10 @@ public static List GetRequestedRoleNames(this HttpContext client) { var claims = client.GetRequestedClaims(); var objectIdClaim = claims.FirstOrDefault(c => c.Type == "oid"); - if (objectIdClaim is null) { return null; } + if (objectIdClaim is null) + { + return null; + } return objectIdClaim.Value; } }