diff --git a/backend/api.test/Client/AreaTests.cs b/backend/api.test/Client/AreaTests.cs index e919c3512..66d244d06 100644 --- a/backend/api.test/Client/AreaTests.cs +++ b/backend/api.test/Client/AreaTests.cs @@ -138,7 +138,7 @@ public async Task AreaTest() } [Fact] - public async Task GetMissionsInAreaTest() + public async Task MissionIsCreatedInArea() { // Arrange // Robot @@ -156,7 +156,7 @@ public async Task GetMissionsInAreaTest() Assert.True(installationResponse.IsSuccessStatusCode); var installations = await installationResponse.Content.ReadFromJsonAsync>(_serializerOptions); Assert.True(installations != null); - var installation = installations[0]; + var installation = installations.Where(installation => installation.InstallationCode == robot.CurrentInstallation?.InstallationCode).First(); // Area string areaUrl = "/areas"; @@ -164,7 +164,7 @@ public async Task GetMissionsInAreaTest() Assert.True(areaResponse.IsSuccessStatusCode); var areas = await areaResponse.Content.ReadFromJsonAsync>(_serializerOptions); Assert.True(areas != null); - var area = areas[0]; + var area = areas.Where(area => area.InstallationCode == installation.InstallationCode).First(); string areaId = area.Id; string testMissionName = "testMissionInAreaTest"; @@ -272,6 +272,9 @@ public async Task SafePositionTest() // Assert Assert.True(missionResponse.IsSuccessStatusCode); + // The endpoint posted to above triggers an event and returns a successful response. + // The test finishes and disposes of objects, but the operations of that event handler are still running, leading to a crash. + await Task.Delay(5000); } [Fact] diff --git a/backend/api.test/Client/MissionTests.cs b/backend/api.test/Client/MissionTests.cs index e1dbb3edb..5737653b4 100644 --- a/backend/api.test/Client/MissionTests.cs +++ b/backend/api.test/Client/MissionTests.cs @@ -41,7 +41,7 @@ public MissionTests(TestWebApplicationFactory factory) ); } - private async Task PostToDb(string postUrl, T stringContent) + private async Task PostToDb(string postUrl, TQueryType stringContent) { var content = new StringContent( JsonSerializer.Serialize(stringContent), @@ -172,7 +172,7 @@ private async Task PostToDb(string postUrl, StringContent content) return responseObject; } - private async Task<(string installationId, string plantId, string deckId, string areaId)> PostAssetInformationToDb(string installationCode, string plantCode, string deckName, string areaName) + private async Task<(Installation installation, Plant plant, Deck deck, Area area)> PostAssetInformationToDb(string installationCode, string plantCode, string deckName, string areaName) { await VerifyNonDuplicateAreaDbNames(installationCode, plantCode, deckName, areaName); @@ -183,12 +183,12 @@ private async Task PostToDb(string postUrl, StringContent content) (var installationContent, var plantContent, var deckContent, var areaContent) = ArrangeAreaPostQueries(installationCode, plantCode, deckName, areaName); - string installationId = (await PostToDb(installationUrl, installationContent)).Id; - string plantId = (await PostToDb(plantUrl, plantContent)).Id; - string deckId = (await PostToDb(deckUrl, deckContent)).Id; - string areaId = (await PostToDb(areaUrl, areaContent)).Id; + var installation = await PostToDb(installationUrl, installationContent); + var plant = await PostToDb(plantUrl, plantContent); + var deck = await PostToDb(deckUrl, deckContent); + var area = await PostToDb(areaUrl, areaContent); - return (installationId, plantId, deckId, areaId); + return (installation, plant, deck, area); } [Fact] @@ -378,19 +378,31 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() string plantCode = "plantScheduleDuplicateCustomMissionDefinitions"; string deckName = "deckScheduleDuplicateCustomMissionDefinitions"; string areaName = "areaScheduleDuplicateCustomMissionDefinitions"; - (_, _, _, _) = await PostAssetInformationToDb(installationCode, plantCode, deckName, areaName); + (var installation, _, _, _) = await PostAssetInformationToDb(installationCode, plantCode, deckName, areaName); string testMissionName = "testMissionScheduleDuplicateCustomMissionDefinitions"; - // Arrange - Create custom mission definition + // Arrange - Create robot + var robotQuery = new CreateRobotQuery + { + IsarId = Guid.NewGuid().ToString(), + Name = "RobotGetNextRun", + SerialNumber = "GetNextRun", + RobotType = RobotType.Robot, + Status = RobotStatus.Available, + Enabled = true, + Host = "localhost", + Port = 3000, + CurrentInstallationCode = installationCode, + CurrentAreaName = null, + VideoStreams = new List() + }; + string robotUrl = "/robots"; - var response = await _client.GetAsync(robotUrl); - Assert.True(response.IsSuccessStatusCode); - var robots = await response.Content.ReadFromJsonAsync>(_serializerOptions); - Assert.True(robots != null); - var robot = robots[0]; + var robot = await PostToDb(robotUrl, robotQuery); string robotId = robot.Id; + // Arrange - Create custom mission definition var query = new CustomMissionQuery { RobotId = robotId, @@ -402,7 +414,7 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() Tasks = [ new() { - RobotPose = new Pose(), + RobotPose = new Pose(new Position(23, 14, 4), new Orientation()), Inspections = [], InspectionTarget = new Position(), TaskOrder = 0 @@ -449,21 +461,33 @@ public async Task ScheduleDuplicateCustomMissionDefinitions() public async Task GetNextRun() { // Arrange - Initialise area - string installationCode = "installationMissionsTest"; - string plantCode = "plantMissionsTest"; - string deckName = "deckMissionsTest"; - string areaName = "areaMissionsTest"; - (string installationId, string plantId, string deckId, string areaId) = await PostAssetInformationToDb(installationCode, plantCode, deckName, areaName); + string installationCode = "installationGetNextRun"; + string plantCode = "plantGetNextRun"; + string deckName = "deckGetNextRun"; + string areaName = "areaGetNextRun"; + (var installation, _, _, _) = await PostAssetInformationToDb(installationCode, plantCode, deckName, areaName); + + // Arrange - Create robot + var robotQuery = new CreateRobotQuery + { + IsarId = Guid.NewGuid().ToString(), + Name = "RobotGetNextRun", + SerialNumber = "GetNextRun", + RobotType = RobotType.Robot, + Status = RobotStatus.Available, + Enabled = true, + Host = "localhost", + Port = 3000, + CurrentInstallationCode = installation.InstallationCode, + CurrentAreaName = areaName, + VideoStreams = new List() + }; - // Arrange - Create custom mission definition string robotUrl = "/robots"; - var response = await _client.GetAsync(robotUrl); - Assert.True(response.IsSuccessStatusCode); - var robots = await response.Content.ReadFromJsonAsync>(_serializerOptions); - Assert.True(robots != null); - var robot = robots[0]; + var robot = await PostToDb(robotUrl, robotQuery); string robotId = robot.Id; + // Arrange - Schedule custom mission - create mission definition string testMissionName = "testMissionNextRun"; var query = new CustomMissionQuery { @@ -490,7 +514,7 @@ public async Task GetNextRun() ); string customMissionsUrl = "/missions/custom"; - response = await _client.PostAsync(customMissionsUrl, content); + var response = await _client.PostAsync(customMissionsUrl, content); Assert.True(response.IsSuccessStatusCode); var missionRun = await response.Content.ReadFromJsonAsync(_serializerOptions); Assert.True(missionRun != null); @@ -565,7 +589,7 @@ public async Task ScheduleDuplicateEchoMissionDefinitions() string plantCode = "plantScheduleDuplicateEchoMissionDefinitions"; string deckName = "deckScheduleDuplicateEchoMissionDefinitions"; string areaName = "areaScheduleDuplicateEchoMissionDefinitions"; - (string installationId, string plantId, string deckId, string areaId) = await PostAssetInformationToDb(installationCode, plantCode, deckName, areaName); + (_, _, _, _) = await PostAssetInformationToDb(installationCode, plantCode, deckName, areaName); // Arrange - Create echo mission definition string robotUrl = "/robots"; @@ -613,5 +637,173 @@ public async Task ScheduleDuplicateEchoMissionDefinitions() Assert.NotNull(missionDefinitions); Assert.NotNull(missionDefinitions.Find(m => m.Id == missionId1)); } + + [Fact] + public async Task MissionDoesNotStartIfRobotIsNotInSameInstallationAsMission() + { + // Arrange - Initialise area + string installationCode = "installationMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission"; + string plantCode = "plantMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission"; + string deckName = "deckMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission"; + string areaName = "areaMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission"; + (var installation, _, _, _) = await PostAssetInformationToDb(installationCode, plantCode, deckName, areaName); + + string testMissionName = "testMissionDoesNotStartIfRobotIsNotInSameInstallationAsMission"; + + // Arrange - Get different installation + string installationUrl = "/installations"; + var installationResponse = await _client.GetAsync(installationUrl); + Assert.True(installationResponse.IsSuccessStatusCode); + var installations = await installationResponse.Content.ReadFromJsonAsync>(_serializerOptions); + Assert.True(installations != null); + var missionInstallation = installations[0]; + + // Arrange - Create robot + var robotQuery = new CreateRobotQuery + { + IsarId = Guid.NewGuid().ToString(), + Name = "RobotGetNextRun", + SerialNumber = "GetNextRun", + RobotType = RobotType.Robot, + Status = RobotStatus.Available, + Enabled = true, + Host = "localhost", + Port = 3000, + CurrentInstallationCode = installationCode, + CurrentAreaName = null, + VideoStreams = new List() + }; + + string robotUrl = "/robots"; + var robot = await PostToDb(robotUrl, robotQuery); + string robotId = robot.Id; + + // Arrange - Create custom mission definition + var query = new CustomMissionQuery + { + RobotId = robotId, + InstallationCode = missionInstallation.InstallationCode, + AreaName = areaName, + DesiredStartTime = DateTime.SpecifyKind(new DateTime(3050, 1, 1), DateTimeKind.Utc), + InspectionFrequency = new TimeSpan(14, 0, 0, 0), + Name = testMissionName, + Tasks = [ + new() + { + RobotPose = new Pose(), + Inspections = [], + InspectionTarget = new Position(), + TaskOrder = 0 + }, + new() + { + RobotPose = new Pose(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f), + Inspections = [], + InspectionTarget = new Position(), + TaskOrder = 1 + } + ] + }; + var content = new StringContent( + JsonSerializer.Serialize(query), + null, + "application/json" + ); + + // Act + string customMissionsUrl = "/missions/custom"; + var response = await _client.PostAsync(customMissionsUrl, content); + Assert.True(response.StatusCode == HttpStatusCode.Conflict); + } + + [Fact] + public async Task MissionFailsIfRobotIsNotInSameDeckAsMission() + { + // Arrange - Initialise area + string installationCode = "installationMissionFailsIfRobotIsNotInSameDeckAsMission"; + string plantCode = "plantMissionFailsIfRobotIsNotInSameDeckAsMission"; + string deckName = "deckMissionFailsIfRobotIsNotInSameDeckAsMission"; + string areaName = "areaMissionFailsIfRobotIsNotInSameDeckAsMission"; + (var installation, _, _, _) = await PostAssetInformationToDb(installationCode, plantCode, deckName, areaName); + + string testMissionName = "testMissionFailsIfRobotIsNotInSameDeckAsMission"; + + // Arrange - Get different area + string areaUrl = "/areas"; + var response = await _client.GetAsync(areaUrl); + Assert.True(response.IsSuccessStatusCode); + var areas = await response.Content.ReadFromJsonAsync>(_serializerOptions); + Assert.True(areas != null); + var areaResponse = areas[0]; + + // Arrange - Create robot + var robotQuery = new CreateRobotQuery + { + IsarId = Guid.NewGuid().ToString(), + Name = "RobotMissionFailsIfRobotIsNotInSameDeckAsMission", + SerialNumber = "GetMissionFailsIfRobotIsNotInSameDeckAsMission", + RobotType = RobotType.Robot, + Status = RobotStatus.Available, + Enabled = true, + Host = "localhost", + Port = 3000, + CurrentInstallationCode = installationCode, + CurrentAreaName = areaName, + VideoStreams = new List() + }; + + string robotUrl = "/robots"; + var robot = await PostToDb(robotUrl, robotQuery); + string robotId = robot.Id; + + // Arrange - Mission Run Query + var query = new CustomMissionQuery + { + RobotId = robotId, + InstallationCode = installation.InstallationCode, + AreaName = areaResponse.AreaName, + DesiredStartTime = DateTime.SpecifyKind(new DateTime(3050, 1, 1), DateTimeKind.Utc), + InspectionFrequency = new TimeSpan(14, 0, 0, 0), + Name = testMissionName, + Tasks = [ + new() + { + RobotPose = new Pose(new Position(1, 9, 4), new Orientation()), + Inspections = [], + InspectionTarget = new Position(), + TaskOrder = 0 + }, + new() + { + RobotPose = new Pose(1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f), + Inspections = [], + InspectionTarget = new Position(), + TaskOrder = 1 + } + ] + }; + var content = new StringContent( + JsonSerializer.Serialize(query), + null, + "application/json" + ); + + // Act + string customMissionsUrl = "/missions/custom"; + var missionResponse = await _client.PostAsync(customMissionsUrl, content); + Assert.True(missionResponse.IsSuccessStatusCode); + var missionRun = await missionResponse.Content.ReadFromJsonAsync(_serializerOptions); + Assert.NotNull(missionRun); + Assert.True(missionRun.Status == MissionStatus.Pending); + + await Task.Delay(2000); + string missionRunByIdUrl = $"/missions/runs/{missionRun.Id}"; + var missionByIdResponse = await _client.GetAsync(missionRunByIdUrl); + Assert.True(missionByIdResponse.IsSuccessStatusCode); + var missionRunAfterUpdate = await missionByIdResponse.Content.ReadFromJsonAsync(_serializerOptions); + Assert.NotNull(missionRunAfterUpdate); + Assert.True(missionRunAfterUpdate.Status == MissionStatus.Cancelled); + } + } } diff --git a/backend/api.test/Database/DatabaseUtilities.cs b/backend/api.test/Database/DatabaseUtilities.cs index ceaedef7a..d2cd4b3e4 100644 --- a/backend/api.test/Database/DatabaseUtilities.cs +++ b/backend/api.test/Database/DatabaseUtilities.cs @@ -32,7 +32,7 @@ public DatabaseUtilities(FlotillaDbContext context) _areaService = new AreaService(context, _installationService, _plantService, _deckService, defaultLocalizationPoseService, _accessRoleService); _missionRunService = new MissionRunService(context, new MockSignalRService(), new Mock>().Object, _accessRoleService); _robotModelService = new RobotModelService(context); - _robotService = new RobotService(context, new Mock>().Object, _robotModelService, new MockSignalRService(), _accessRoleService, _installationService); + _robotService = new RobotService(context, new Mock>().Object, _robotModelService, new MockSignalRService(), _accessRoleService, _installationService, _areaService); } public void Dispose() @@ -116,7 +116,7 @@ public async Task NewArea(string installationCode, string plantCode, strin return await _areaService.Create(createAreaQuery); } - public async Task NewRobot(RobotStatus status, Installation installation) + public async Task NewRobot(RobotStatus status, Installation installation, Area? area = null) { var createRobotQuery = new CreateRobotQuery { @@ -125,6 +125,7 @@ public async Task NewRobot(RobotStatus status, Installation installation) RobotType = RobotType.Robot, SerialNumber = "0001", CurrentInstallationCode = installation.InstallationCode, + CurrentAreaName = area?.Name, VideoStreams = new List(), Host = "localhost", Port = 3000, @@ -133,7 +134,7 @@ public async Task NewRobot(RobotStatus status, Installation installation) }; var robotModel = await _robotModelService.ReadByRobotType(createRobotQuery.RobotType); - var robot = new Robot(createRobotQuery, installation) + var robot = new Robot(createRobotQuery, installation, area) { Model = robotModel! }; diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs index 92388f898..ee8579a62 100644 --- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs +++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs @@ -35,17 +35,14 @@ public class TestMissionEventHandler : IDisposable private readonly MissionEventHandler _missionEventHandler; private readonly MissionRunService _missionRunService; private readonly IMissionSchedulingService _missionSchedulingService; - -#pragma warning disable IDE0052 private readonly MqttEventHandler _mqttEventHandler; -#pragma warning restore IDE0052 - private readonly MqttService _mqttService; private readonly PlantService _plantService; private readonly RobotControllerMock _robotControllerMock; private readonly RobotModelService _robotModelService; private readonly RobotService _robotService; private readonly ISignalRService _signalRService; + private readonly LocalizationService _localizationService; private readonly DatabaseUtilities _databaseUtilities; private readonly AccessRoleService _accessRoleService; @@ -57,6 +54,7 @@ public TestMissionEventHandler(DatabaseFixture fixture) var missionLogger = new Mock>().Object; var missionSchedulingServiceLogger = new Mock>().Object; var robotServiceLogger = new Mock>().Object; + var localizationServiceLogger = new Mock>().Object; var configuration = WebApplication.CreateBuilder().Configuration; @@ -75,9 +73,10 @@ public TestMissionEventHandler(DatabaseFixture fixture) _plantService = new PlantService(_context, _installationService, _accessRoleService); _deckService = new DeckService(_context, _defaultLocalisationPoseService, _installationService, _plantService, _accessRoleService); _areaService = new AreaService(_context, _installationService, _plantService, _deckService, _defaultLocalisationPoseService, _accessRoleService); - _robotService = new RobotService(_context, robotServiceLogger, _robotModelService, _signalRService, _accessRoleService, _installationService); + _robotService = new RobotService(_context, robotServiceLogger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService); _missionSchedulingService = new MissionSchedulingService(missionSchedulingServiceLogger, _missionRunService, _robotService, _robotControllerMock.Mock.Object, _areaService, _isarServiceMock); + _localizationService = new LocalizationService(localizationServiceLogger, _robotService, _missionRunService, _installationService, _areaService); _databaseUtilities = new DatabaseUtilities(_context); @@ -99,6 +98,9 @@ public TestMissionEventHandler(DatabaseFixture fixture) mockServiceProvider .Setup(p => p.GetService(typeof(FlotillaDbContext))) .Returns(_context); + mockServiceProvider + .Setup(p => p.GetService(typeof(ILocalizationService))) + .Returns(_localizationService); // Mock service injector var mockScope = new Mock(); @@ -125,7 +127,7 @@ public async void ScheduledMissionStartedWhenSystemIsAvailable() var plant = await _databaseUtilities.NewPlant(installation.InstallationCode); var deck = await _databaseUtilities.NewDeck(installation.InstallationCode, plant.PlantCode); var area = await _databaseUtilities.NewArea(installation.InstallationCode, plant.PlantCode, deck.Name); - var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); + var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, area); var missionRun = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, area, false); SetupMocksForRobotController(robot, missionRun); @@ -146,7 +148,7 @@ public async void SecondScheduledMissionQueuedIfRobotIsBusy() var plant = await _databaseUtilities.NewPlant(installation.InstallationCode); var deck = await _databaseUtilities.NewDeck(installation.InstallationCode, plant.PlantCode); var area = await _databaseUtilities.NewArea(installation.InstallationCode, plant.PlantCode, deck.Name); - var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); + var robot = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, area); var missionRunOne = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, area, false); var missionRunTwo = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, area, false); @@ -171,7 +173,7 @@ public async void NewMissionIsStartedWhenRobotBecomesAvailable() var plant = await _databaseUtilities.NewPlant(installation.InstallationCode); var deck = await _databaseUtilities.NewDeck(installation.InstallationCode, plant.PlantCode); var area = await _databaseUtilities.NewArea(installation.InstallationCode, plant.PlantCode, deck.Name); - var robot = await _databaseUtilities.NewRobot(RobotStatus.Busy, installation); + var robot = await _databaseUtilities.NewRobot(RobotStatus.Busy, installation, area); var missionRun = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robot, area, false); SetupMocksForRobotController(robot, missionRun); @@ -248,8 +250,8 @@ public async void MissionRunIsStartedForOtherAvailableRobotIfOneRobotHasAnOngoin var plant = await _databaseUtilities.NewPlant(installation.InstallationCode); var deck = await _databaseUtilities.NewDeck(installation.InstallationCode, plant.PlantCode); var area = await _databaseUtilities.NewArea(installation.InstallationCode, plant.PlantCode, deck.Name); - var robotOne = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); - var robotTwo = await _databaseUtilities.NewRobot(RobotStatus.Available, installation); + var robotOne = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, area); + var robotTwo = await _databaseUtilities.NewRobot(RobotStatus.Available, installation, area); var missionRunOne = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robotOne, area, false); var missionRunTwo = await _databaseUtilities.NewMissionRun(installation.InstallationCode, robotTwo, area, false); diff --git a/backend/api.test/Mocks/CustomMissionServiceMock.cs b/backend/api.test/Mocks/CustomMissionServiceMock.cs index 342da6740..b95831901 100644 --- a/backend/api.test/Mocks/CustomMissionServiceMock.cs +++ b/backend/api.test/Mocks/CustomMissionServiceMock.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading.Tasks; +using Api.Controllers.Models; using Api.Database.Models; - namespace Api.Services { public class MockCustomMissionService : ICustomMissionService @@ -36,16 +37,19 @@ public Task UploadSource(List tasks) public string CalculateHashFromTasks(IList tasks) { - List genericTasks = []; - foreach (var task in tasks) - { - var taskCopy = new MissionTask(task); - genericTasks.Add(taskCopy); - } + IList genericTasks = tasks.Select(task => new MissionTask(task)).ToList(); string json = JsonSerializer.Serialize(genericTasks); byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(json)); return BitConverter.ToString(hash).Replace("-", "", StringComparison.CurrentCulture).ToUpperInvariant(); } + +#pragma warning disable IDE0060, CA1822 // Remove unused parameter + public async Task QueueCustomMissionRun(CustomMissionQuery customMissionQuery, MissionDefinition customMissionDefinition, Robot robot, IList missionTasks) + { + await Task.CompletedTask; + return new MissionRun(); + } +#pragma warning restore IDE0060, CA1822 // Remove unused parameter } } diff --git a/backend/api.test/Services/RobotService.cs b/backend/api.test/Services/RobotService.cs index ba84d2d76..05809ba0a 100644 --- a/backend/api.test/Services/RobotService.cs +++ b/backend/api.test/Services/RobotService.cs @@ -21,6 +21,10 @@ public class RobotServiceTest : IDisposable private readonly ISignalRService _signalRService; private readonly IAccessRoleService _accessRoleService; private readonly IInstallationService _installationService; + private readonly IPlantService _plantService; + private readonly IDefaultLocalizationPoseService _defaultLocalizationPoseService; + private readonly IDeckService _deckService; + private readonly IAreaService _areaService; public RobotServiceTest(DatabaseFixture fixture) { @@ -30,6 +34,10 @@ public RobotServiceTest(DatabaseFixture fixture) _signalRService = new MockSignalRService(); _accessRoleService = new AccessRoleService(_context, new HttpContextAccessor()); _installationService = new InstallationService(_context, _accessRoleService); + _plantService = new PlantService(_context, _installationService, _accessRoleService); + _defaultLocalizationPoseService = new DefaultLocalizationPoseService(_context); + _deckService = new DeckService(_context, _defaultLocalizationPoseService, _installationService, _plantService, _accessRoleService); + _areaService = new AreaService(_context, _installationService, _plantService, _deckService, _defaultLocalizationPoseService, _accessRoleService); } public void Dispose() @@ -41,7 +49,7 @@ public void Dispose() [Fact] public async Task ReadAll() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService); + var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService); var robots = await robotService.ReadAll(); Assert.True(robots.Any()); @@ -50,7 +58,7 @@ public async Task ReadAll() [Fact] public async Task Read() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService); + var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService); var robots = await robotService.ReadAll(); var firstRobot = robots.First(); var robotById = await robotService.ReadById(firstRobot.Id); @@ -61,7 +69,7 @@ public async Task Read() [Fact] public async Task ReadIdDoesNotExist() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService); + var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService); var robot = await robotService.ReadById("some_id_that_does_not_exist"); Assert.Null(robot); } @@ -69,7 +77,7 @@ public async Task ReadIdDoesNotExist() [Fact] public async Task Create() { - var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService); + var robotService = new RobotService(_context, _logger, _robotModelService, _signalRService, _accessRoleService, _installationService, _areaService); var installationService = new InstallationService(_context, _accessRoleService); var installation = await installationService.Create(new CreateInstallationQuery diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index 6b7ade28e..d9be88ee9 100644 --- a/backend/api/Controllers/MissionSchedulingController.cs +++ b/backend/api/Controllers/MissionSchedulingController.cs @@ -12,6 +12,7 @@ namespace Api.Controllers [Route("missions")] public class MissionSchedulingController( IMissionDefinitionService missionDefinitionService, + ICustomMissionSchedulingService customMissionSchedulingService, IMissionRunService missionRunService, IInstallationService installationService, IRobotService robotService, @@ -19,10 +20,11 @@ public class MissionSchedulingController( ILogger logger, IMapService mapService, IStidService stidService, - ISourceService sourceService, - ICustomMissionSchedulingService customMissionSchedulingService + ILocalizationService localizationService, + ISourceService sourceService ) : ControllerBase { + /// /// Schedule an existing mission definition /// @@ -42,24 +44,17 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery ) { var robot = await robotService.ReadById(scheduledMissionQuery.RobotId); - if (robot is null) - { - return NotFound($"Could not find robot with id {scheduledMissionQuery.RobotId}"); - } + if (robot is null) { return NotFound($"Could not find robot with id {scheduledMissionQuery.RobotId}"); } var missionDefinition = await missionDefinitionService.ReadById(scheduledMissionQuery.MissionDefinitionId); - if (missionDefinition == null) - { - return NotFound("Mission definition not found"); - } + if (missionDefinition == null) { return NotFound("Mission definition not found"); } - List? missionTasks; - missionTasks = await missionDefinitionService.GetTasksFromSource(missionDefinition.Source, missionDefinition.InstallationCode); + try { await localizationService.EnsureRobotIsOnSameInstallationAsMission(robot, missionDefinition); } + catch (InstallationNotFoundException e) { return NotFound(e.Message); } + catch (RobotNotInSameInstallationAsMissionException e) { return Conflict(e.Message); } - if (missionTasks == null) - { - return NotFound("No mission tasks were found for the requested mission"); - } + var missionTasks = await missionDefinitionService.GetTasksFromSource(missionDefinition.Source, missionDefinition.InstallationCode); + if (missionTasks == null) return NotFound("No mission tasks were found for the requested mission"); var missionRun = new MissionRun { @@ -72,7 +67,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery Tasks = missionTasks, InstallationCode = missionDefinition.InstallationCode, Area = missionDefinition.Area, - Map = new MapMetadata() + Map = missionDefinition.Area?.MapMetadata ?? new MapMetadata() }; await mapService.AssignMapToMission(missionRun); @@ -270,6 +265,10 @@ [FromBody] CustomMissionQuery customMissionQuery try { customMissionDefinition = await customMissionSchedulingService.FindExistingOrCreateCustomMissionDefinition(customMissionQuery, missionTasks); } catch (SourceException e) { return StatusCode(StatusCodes.Status502BadGateway, 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 { newMissionRun = await customMissionSchedulingService.QueueCustomMissionRun(customMissionQuery, customMissionDefinition.Id, robot.Id, missionTasks); } catch (Exception e) when (e is RobotNotFoundException or MissionNotFoundException) { return NotFound(e.Message); } diff --git a/backend/api/Controllers/Models/CreateRobotQuery.cs b/backend/api/Controllers/Models/CreateRobotQuery.cs index 7ca409de7..debbd6b8c 100644 --- a/backend/api/Controllers/Models/CreateRobotQuery.cs +++ b/backend/api/Controllers/Models/CreateRobotQuery.cs @@ -14,6 +14,8 @@ public struct CreateRobotQuery public string CurrentInstallationCode { get; set; } + public string? CurrentAreaName { get; set; } + public IList VideoStreams { get; set; } public string Host { get; set; } diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index ba624e165..2cf8a23ff 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -40,7 +40,8 @@ public static class InitDb TagId = "Tagid here", EchoTagLink = new Uri("https://www.I-am-echo-stid-tag-url.com"), InspectionTarget = new Position(), - RobotPose = new Pose() + RobotPose = new Pose(), + Type = MissionTaskType.Inspection }; private static List GetAccessRoles() @@ -67,11 +68,16 @@ private static List GetInstallations() InstallationCode = "HUA" }; - // Adding another installation makes the tests fail currently + var installation2 = new Installation + { + Id = Guid.NewGuid().ToString(), + Name = "Kårstø", + InstallationCode = "KAA" + }; return new List(new[] { - installation1 + installation1, installation2 }); } @@ -85,9 +91,17 @@ private static List GetPlants() PlantCode = "HUA" }; + var plant2 = new Plant + { + Id = Guid.NewGuid().ToString(), + Installation = installations[0], + Name = "Kårstø", + PlantCode = "Kårstø" + }; + return new List(new[] { - plant1 + plant1, plant2 }); } @@ -155,7 +169,7 @@ private static List GetAreas() Name = "AP320", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List() + SafePositions = new List(new[] { new SafePosition() }) }; var area2 = new Area @@ -167,7 +181,7 @@ private static List GetAreas() Name = "AP330", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List() + SafePositions = new List(new[] { new SafePosition() }) }; var area3 = new Area @@ -179,10 +193,7 @@ private static List GetAreas() Name = "testArea", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List - { - new() - } + SafePositions = new List(new[] { new SafePosition() }) }; var area4 = new Area @@ -194,7 +205,7 @@ private static List GetAreas() Name = "testArea2", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List() + SafePositions = new List(new[] { new SafePosition() }) }; var area5 = new Area @@ -206,7 +217,7 @@ private static List GetAreas() Name = "testArea3", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List() + SafePositions = new List(new[] { new SafePosition() }) }; var area6 = new Area @@ -218,7 +229,7 @@ private static List GetAreas() Name = "testArea4", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List() + SafePositions = new List(new[] { new SafePosition() }) }; var areaHuldraHB = new Area @@ -644,10 +655,7 @@ public static void PopulateDb(FlotillaDbContext context) var task = ExampleTask; task.Inspections.Add(Inspection); task.Inspections.Add(Inspection2); - var tasks = new List - { - task - }; + var tasks = new List { task }; missionRun.Tasks = tasks; } context.AddRange(robots); @@ -658,6 +666,7 @@ public static void PopulateDb(FlotillaDbContext context) context.AddRange(decks); context.AddRange(areas); context.AddRange(accessRoles); + context.SaveChanges(); } } diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 4fed62d63..e39fb8d0a 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -188,6 +188,10 @@ var inspection in task.Inspections.Where(inspection => !inspection.IsCompleted) } } } + + public bool IsLocalizationMission() { return Tasks is [{ Type: MissionTaskType.Localization }]; } + + public bool IsDriveToMission() { return Tasks is [{ Type: MissionTaskType.DriveTo }]; } } public enum MissionStatus diff --git a/backend/api/Database/Models/MissionTask.cs b/backend/api/Database/Models/MissionTask.cs index 996430348..95be9d260 100644 --- a/backend/api/Database/Models/MissionTask.cs +++ b/backend/api/Database/Models/MissionTask.cs @@ -1,7 +1,9 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using System.Globalization; using Api.Controllers.Models; using Api.Services.Models; +using Api.Utilities; #pragma warning disable CS8618 namespace Api.Database.Models { @@ -26,6 +28,7 @@ public MissionTask(EchoTag echoTag, Position tagPosition) EchoPoseId = echoTag.PoseId; TaskOrder = echoTag.PlanOrder; Status = TaskStatus.NotStarted; + Type = MissionTaskType.Inspection; } // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized @@ -40,6 +43,34 @@ public MissionTask(CustomTaskQuery taskQuery) RobotPose = taskQuery.RobotPose; TaskOrder = taskQuery.TaskOrder; Status = TaskStatus.NotStarted; + Type = MissionTaskType.Inspection; + } + + public MissionTask(Pose robotPose, MissionTaskType type) + { + switch (type) + { + case MissionTaskType.Localization: + Type = type; + Description = "Localization"; + RobotPose = robotPose; + TaskOrder = 0; + Status = TaskStatus.NotStarted; + InspectionTarget = new Position(); + Inspections = new List(); + break; + case MissionTaskType.DriveTo: + Type = type; + Description = "Return to home"; + RobotPose = robotPose; + TaskOrder = 0; + Status = TaskStatus.NotStarted; + InspectionTarget = new Position(); + Inspections = new List(); + break; + default: + throw new MissionTaskNotFoundException("MissionTaskType should be Localization or DriveTo"); + } } // Creates a blank deepcopy of the provided task @@ -68,6 +99,8 @@ public MissionTask(MissionTask copy) [Required] public int TaskOrder { get; set; } + public MissionTaskType Type { get; set; } + [MaxLength(200)] public string? TagId { get; set; } @@ -146,6 +179,12 @@ public void UpdateStatus(IsarTaskStatus isarStatus) && inspection.IsarStepId.Equals(isarStepId, StringComparison.Ordinal) ); } + + public static string ConvertMissionTaskTypeToIsarTaskType(MissionTaskType missionTaskType) + { + if (missionTaskType == MissionTaskType.DriveTo) { return "drive_to"; } + else { return missionTaskType.ToString().ToLower(CultureInfo.CurrentCulture); } + } } public enum TaskStatus @@ -158,4 +197,11 @@ public enum TaskStatus Cancelled, Paused } + + public enum MissionTaskType + { + Inspection, + Localization, + DriveTo + } } diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 7690f2629..60ea5f2a6 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -11,6 +11,8 @@ public class MissionEventHandler : EventHandlerBase // The mutex is used to ensure multiple missions aren't attempted scheduled simultaneously whenever multiple mission runs are created private readonly Semaphore _scheduleMissionSemaphore = new(1, 1); + private readonly Semaphore _scheduleLocalizationSemaphore = new(1, 1); + private readonly IServiceScopeFactory _scopeFactory; public MissionEventHandler( @@ -23,12 +25,15 @@ IServiceScopeFactory scopeFactory Subscribe(); } - private IMissionRunService MissionService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private IRobotService RobotService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IReturnToHomeService ReturnToHomeService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + + private ILocalizationService LocalizationService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IAreaService AreaService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private IMissionSchedulingService MissionScheduling => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); @@ -59,13 +64,36 @@ private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArg _logger.LogInformation("Triggered MissionRunCreated event for mission run ID: {MissionRunId}", e.MissionRunId); var missionRun = await MissionService.ReadById(e.MissionRunId); - if (missionRun == null) { _logger.LogError("Mission run with ID: {MissionRunId} was not found in the database", e.MissionRunId); return; } + + _scheduleLocalizationSemaphore.WaitOne(); + + string? localizationMissionRunId = null; + try { localizationMissionRunId = await LocalizationService.EnsureRobotIsCorrectlyLocalized(missionRun.Robot, missionRun); } + catch (Exception ex) when ( + ex is AreaNotFoundException + or DeckNotFoundException + or RobotNotAvailableException + or RobotLocalizationException + or RobotNotFoundException + or IsarCommunicationException + ) + { + _logger.LogError("Mission run {MissionRunId} will be cancelled as robot {RobotId} was not correctly localized", missionRun.Id, missionRun.Robot.Id); + missionRun.Status = MissionStatus.Cancelled; + await MissionService.Update(missionRun); + return; + } + finally { _scheduleLocalizationSemaphore.Release(); } + + string missionRunIdToStart = missionRun.Id; + if (localizationMissionRunId is not null) missionRunIdToStart = localizationMissionRunId; + if (MissionScheduling.MissionRunQueueIsEmpty(await MissionService.ReadMissionRunQueue(missionRun.Robot.Id))) { _logger.LogInformation("Mission run {MissionRunId} was not started as there are no mission runs on the queue", e.MissionRunId); @@ -73,8 +101,9 @@ private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArg } _scheduleMissionSemaphore.WaitOne(); - await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun.Id); - _scheduleMissionSemaphore.Release(); + try { await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRunIdToStart); } + catch (MissionRunNotFoundException) { } + finally { _scheduleMissionSemaphore.Release(); } } private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) @@ -87,9 +116,44 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) return; } + //TODO Separate into functions to make it more readable + if (!await LocalizationService.RobotIsLocalized(robot.Id)) + { + try { await LocalizationService.EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(robot.Id); } + catch (Exception ex) when (ex is LocalizationFailedException or RobotNotFoundException or MissionNotFoundException or OngoingMissionNotLocalizationException or TimeoutException) + { + _logger.LogError("Could not confirm that the robot {RobotId} was correctly localized and the scheduled missions for the deck will be cancelled", robot.Id); + try { await MissionScheduling.CancelAllScheduledMissions(robot.Id); } + catch (RobotNotFoundException) + { + _logger.LogError("Failed to cancel scheduled missions for robot {RobotId}", robot.Id); + return; + } + } + } + if (MissionScheduling.MissionRunQueueIsEmpty(await MissionService.ReadMissionRunQueue(robot.Id))) { _logger.LogInformation("The robot was changed to available but there are no mission runs in the queue to be scheduled"); + + var lastExecutedMissionRun = await MissionService.ReadLastExecutedMissionRunByRobot(robot.Id); + if (lastExecutedMissionRun is null) + { + _logger.LogError("Could not find last executed mission run for robot"); + return; + } + + if (!lastExecutedMissionRun.IsDriveToMission()) + { + try { await ReturnToHomeService.ScheduleReturnToHomeMissionRun(robot.Id); } + catch (Exception ex) when (ex is RobotNotFoundException or AreaNotFoundException or DeckNotFoundException or PoseNotFoundException) + { + //TODO Create an issue on sending a warning to the frontend that the return to home mission could not be scheduled + await RobotService.UpdateCurrentArea(robot.Id, null); + return; + } + } + else { await RobotService.UpdateCurrentArea(robot.Id, null); } return; } @@ -104,7 +168,8 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) } _scheduleMissionSemaphore.WaitOne(); - await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun.Id); + try { await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun.Id); } + catch (MissionRunNotFoundException) { return; } _scheduleMissionSemaphore.Release(); } diff --git a/backend/api/Migrations/20231018095954_AddTypeToMissionTask.Designer.cs b/backend/api/Migrations/20231018095954_AddTypeToMissionTask.Designer.cs new file mode 100644 index 000000000..148d639a3 --- /dev/null +++ b/backend/api/Migrations/20231018095954_AddTypeToMissionTask.Designer.cs @@ -0,0 +1,1260 @@ +// +using System; +using Api.Database.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(FlotillaDbContext))] + [Migration("20231018095954_AddTypeToMissionTask")] + partial class AddTypeToMissionTask + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DeckId") + .HasColumnType("text"); + + b.Property("DefaultLocalizationPoseId") + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DeckId"); + + b.HasIndex("DefaultLocalizationPoseId"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("Areas"); + }); + + modelBuilder.Entity("Api.Database.Models.Deck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DefaultLocalizationPoseId") + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DefaultLocalizationPoseId"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("Decks"); + }); + + modelBuilder.Entity("Api.Database.Models.DefaultLocalizationPose", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DefaultLocalizationPoses"); + }); + + modelBuilder.Entity("Api.Database.Models.Installation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationCode") + .IsUnique(); + + b.ToTable("Installations"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AreaId") + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("InspectionFrequency") + .HasColumnType("bigint"); + + b.Property("InstallationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("LastRunId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("LastRunId"); + + b.HasIndex("SourceId"); + + b.ToTable("MissionDefinitions"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AreaId") + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Description") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("DesiredStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedDuration") + .HasColumnType("bigint"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsarMissionId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusReason") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("RobotId"); + + b.ToTable("MissionRuns"); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantCode") + .IsUnique(); + + b.ToTable("Plants"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("BatteryLevel") + .HasColumnType("real"); + + b.Property("CurrentAreaId") + .HasColumnType("text"); + + b.Property("CurrentInstallation") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentMissionId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsarId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("PressureLevel") + .HasColumnType("real"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrentAreaId"); + + b.HasIndex("ModelId"); + + b.ToTable("Robots"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotBatteryTimeseries", b => + { + b.Property("BatteryLevel") + .HasColumnType("real"); + + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.ToTable("RobotBatteryTimeseries"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AverageDurationPerTag") + .HasColumnType("real"); + + b.Property("BatteryWarningThreshold") + .HasColumnType("real"); + + b.Property("LowerPressureWarningThreshold") + .HasColumnType("real"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpperPressureWarningThreshold") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("Type") + .IsUnique(); + + b.ToTable("RobotModels"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotPoseTimeseries", b => + { + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("OrientationW") + .HasColumnType("real"); + + b.Property("OrientationX") + .HasColumnType("real"); + + b.Property("OrientationY") + .HasColumnType("real"); + + b.Property("OrientationZ") + .HasColumnType("real"); + + b.Property("PositionX") + .HasColumnType("real"); + + b.Property("PositionY") + .HasColumnType("real"); + + b.Property("PositionZ") + .HasColumnType("real"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.ToTable("RobotPoseTimeseries"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotPressureTimeseries", b => + { + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("Pressure") + .HasColumnType("real"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.ToTable("RobotPressureTimeseries"); + }); + + modelBuilder.Entity("Api.Database.Models.SafePosition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AreaId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.ToTable("SafePositions"); + }); + + modelBuilder.Entity("Api.Database.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.HasOne("Api.Database.Models.Deck", "Deck") + .WithMany() + .HasForeignKey("DeckId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Api.Database.Models.DefaultLocalizationPose", "DefaultLocalizationPose") + .WithMany() + .HasForeignKey("DefaultLocalizationPoseId"); + + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.MapMetadata", "MapMetadata", b1 => + { + b1.Property("AreaId") + .HasColumnType("text"); + + b1.Property("MapName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("AreaId"); + + b1.ToTable("Areas"); + + b1.WithOwner() + .HasForeignKey("AreaId"); + + b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("text"); + + b2.Property("X1") + .HasColumnType("double precision"); + + b2.Property("X2") + .HasColumnType("double precision"); + + b2.Property("Y1") + .HasColumnType("double precision"); + + b2.Property("Y2") + .HasColumnType("double precision"); + + b2.Property("Z1") + .HasColumnType("double precision"); + + b2.Property("Z2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("text"); + + b2.Property("C1") + .HasColumnType("double precision"); + + b2.Property("C2") + .HasColumnType("double precision"); + + b2.Property("D1") + .HasColumnType("double precision"); + + b2.Property("D2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.Navigation("Boundary") + .IsRequired(); + + b1.Navigation("TransformationMatrices") + .IsRequired(); + }); + + b.Navigation("Deck"); + + b.Navigation("DefaultLocalizationPose"); + + b.Navigation("Installation"); + + b.Navigation("MapMetadata") + .IsRequired(); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.Deck", b => + { + b.HasOne("Api.Database.Models.DefaultLocalizationPose", "DefaultLocalizationPose") + .WithMany() + .HasForeignKey("DefaultLocalizationPoseId"); + + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DefaultLocalizationPose"); + + b.Navigation("Installation"); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.DefaultLocalizationPose", b => + { + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("DefaultLocalizationPoseId") + .HasColumnType("text"); + + b1.HasKey("DefaultLocalizationPoseId"); + + b1.ToTable("DefaultLocalizationPoses"); + + b1.WithOwner() + .HasForeignKey("DefaultLocalizationPoseId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseDefaultLocalizationPoseId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseDefaultLocalizationPoseId"); + + b2.ToTable("DefaultLocalizationPoses"); + + b2.WithOwner() + .HasForeignKey("PoseDefaultLocalizationPoseId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseDefaultLocalizationPoseId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseDefaultLocalizationPoseId"); + + b2.ToTable("DefaultLocalizationPoses"); + + b2.WithOwner() + .HasForeignKey("PoseDefaultLocalizationPoseId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("Pose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId"); + + b.HasOne("Api.Database.Models.MissionRun", "LastRun") + .WithMany() + .HasForeignKey("LastRunId"); + + b.HasOne("Api.Database.Models.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Area"); + + b.Navigation("LastRun"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId"); + + b.HasOne("Api.Database.Models.Robot", "Robot") + .WithMany() + .HasForeignKey("RobotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.MapMetadata", "Map", b1 => + { + b1.Property("MissionRunId") + .HasColumnType("text"); + + b1.Property("MapName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("MissionRunId"); + + b1.ToTable("MissionRuns"); + + b1.WithOwner() + .HasForeignKey("MissionRunId"); + + b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => + { + b2.Property("MapMetadataMissionRunId") + .HasColumnType("text"); + + b2.Property("X1") + .HasColumnType("double precision"); + + b2.Property("X2") + .HasColumnType("double precision"); + + b2.Property("Y1") + .HasColumnType("double precision"); + + b2.Property("Y2") + .HasColumnType("double precision"); + + b2.Property("Z1") + .HasColumnType("double precision"); + + b2.Property("Z2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataMissionRunId"); + + b2.ToTable("MissionRuns"); + + b2.WithOwner() + .HasForeignKey("MapMetadataMissionRunId"); + }); + + b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => + { + b2.Property("MapMetadataMissionRunId") + .HasColumnType("text"); + + b2.Property("C1") + .HasColumnType("double precision"); + + b2.Property("C2") + .HasColumnType("double precision"); + + b2.Property("D1") + .HasColumnType("double precision"); + + b2.Property("D2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataMissionRunId"); + + b2.ToTable("MissionRuns"); + + b2.WithOwner() + .HasForeignKey("MapMetadataMissionRunId"); + }); + + b1.Navigation("Boundary") + .IsRequired(); + + b1.Navigation("TransformationMatrices") + .IsRequired(); + }); + + b.OwnsMany("Api.Database.Models.MissionTask", "Tasks", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b1.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b1.Property("EchoPoseId") + .HasColumnType("integer"); + + b1.Property("EchoTagLink") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("IsarTaskId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.Property("MissionRunId") + .IsRequired() + .HasColumnType("text"); + + b1.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b1.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b1.Property("TagId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.Property("TaskOrder") + .HasColumnType("integer"); + + b1.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b1.HasKey("Id"); + + b1.HasIndex("MissionRunId"); + + b1.ToTable("MissionTask"); + + b1.WithOwner() + .HasForeignKey("MissionRunId"); + + b1.OwnsMany("Api.Database.Models.Inspection", "Inspections", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b2.Property("AnalysisType") + .HasColumnType("text"); + + b2.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b2.Property("InspectionType") + .IsRequired() + .HasColumnType("text"); + + b2.Property("InspectionUrl") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b2.Property("IsarStepId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b2.Property("MissionTaskId") + .IsRequired() + .HasColumnType("text"); + + b2.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b2.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b2.Property("VideoDuration") + .HasColumnType("real"); + + b2.HasKey("Id"); + + b2.HasIndex("MissionTaskId"); + + b2.ToTable("Inspection"); + + b2.WithOwner() + .HasForeignKey("MissionTaskId"); + + b2.OwnsOne("Api.Database.Models.Position", "InspectionTarget", b3 => + { + b3.Property("InspectionId") + .HasColumnType("text"); + + b3.Property("X") + .HasColumnType("real"); + + b3.Property("Y") + .HasColumnType("real"); + + b3.Property("Z") + .HasColumnType("real"); + + b3.HasKey("InspectionId"); + + b3.ToTable("Inspection"); + + b3.WithOwner() + .HasForeignKey("InspectionId"); + }); + + b2.Navigation("InspectionTarget") + .IsRequired(); + }); + + b1.OwnsOne("Api.Database.Models.Position", "InspectionTarget", b2 => + { + b2.Property("MissionTaskId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("MissionTaskId"); + + b2.ToTable("MissionTask"); + + b2.WithOwner() + .HasForeignKey("MissionTaskId"); + }); + + b1.OwnsOne("Api.Database.Models.Pose", "RobotPose", b2 => + { + b2.Property("MissionTaskId") + .HasColumnType("text"); + + b2.HasKey("MissionTaskId"); + + b2.ToTable("MissionTask"); + + b2.WithOwner() + .HasForeignKey("MissionTaskId"); + + b2.OwnsOne("Api.Database.Models.Orientation", "Orientation", b3 => + { + b3.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b3.Property("W") + .HasColumnType("real"); + + b3.Property("X") + .HasColumnType("real"); + + b3.Property("Y") + .HasColumnType("real"); + + b3.Property("Z") + .HasColumnType("real"); + + b3.HasKey("PoseMissionTaskId"); + + b3.ToTable("MissionTask"); + + b3.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b2.OwnsOne("Api.Database.Models.Position", "Position", b3 => + { + b3.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b3.Property("X") + .HasColumnType("real"); + + b3.Property("Y") + .HasColumnType("real"); + + b3.Property("Z") + .HasColumnType("real"); + + b3.HasKey("PoseMissionTaskId"); + + b3.ToTable("MissionTask"); + + b3.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b2.Navigation("Orientation") + .IsRequired(); + + b2.Navigation("Position") + .IsRequired(); + }); + + b1.Navigation("InspectionTarget") + .IsRequired(); + + b1.Navigation("Inspections"); + + b1.Navigation("RobotPose") + .IsRequired(); + }); + + b.Navigation("Area"); + + b.Navigation("Map"); + + b.Navigation("Robot"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Installation"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.HasOne("Api.Database.Models.Area", "CurrentArea") + .WithMany() + .HasForeignKey("CurrentAreaId"); + + b.HasOne("Api.Database.Models.RobotModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("RobotId") + .HasColumnType("text"); + + b1.HasKey("RobotId"); + + b1.ToTable("Robots"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseRobotId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseRobotId"); + + b2.ToTable("Robots"); + + b2.WithOwner() + .HasForeignKey("PoseRobotId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseRobotId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseRobotId"); + + b2.ToTable("Robots"); + + b2.WithOwner() + .HasForeignKey("PoseRobotId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.OwnsMany("Api.Database.Models.VideoStream", "VideoStreams", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b1.Property("ShouldRotate270Clockwise") + .HasColumnType("boolean"); + + b1.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b1.Property("Url") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("Id"); + + b1.HasIndex("RobotId"); + + b1.ToTable("VideoStream"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + }); + + b.Navigation("CurrentArea"); + + b.Navigation("Model"); + + b.Navigation("Pose") + .IsRequired(); + + b.Navigation("VideoStreams"); + }); + + modelBuilder.Entity("Api.Database.Models.SafePosition", b => + { + b.HasOne("Api.Database.Models.Area", null) + .WithMany("SafePositions") + .HasForeignKey("AreaId"); + + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("SafePositionId") + .HasColumnType("text"); + + b1.HasKey("SafePositionId"); + + b1.ToTable("SafePositions"); + + b1.WithOwner() + .HasForeignKey("SafePositionId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseSafePositionId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseSafePositionId"); + + b2.ToTable("SafePositions"); + + b2.WithOwner() + .HasForeignKey("PoseSafePositionId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseSafePositionId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseSafePositionId"); + + b2.ToTable("SafePositions"); + + b2.WithOwner() + .HasForeignKey("PoseSafePositionId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("Pose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.Navigation("SafePositions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/api/Migrations/20231018095954_AddTypeToMissionTask.cs b/backend/api/Migrations/20231018095954_AddTypeToMissionTask.cs new file mode 100644 index 000000000..c9529e8e7 --- /dev/null +++ b/backend/api/Migrations/20231018095954_AddTypeToMissionTask.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class AddTypeToMissionTask : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Type", + table: "MissionTask", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Type", + table: "MissionTask"); + } + } +} diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 547e2087b..8dc423ca8 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -74,6 +74,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -85,13 +87,9 @@ .GetValue("UseInMemoryDatabase"); if (useInMemoryDatabase) -{ builder.Services.AddScoped(); -} else -{ builder.Services.AddScoped(); -} builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/backend/api/Services/ActionServices/CustomMissionSchedulingService.cs b/backend/api/Services/ActionServices/CustomMissionSchedulingService.cs index 507e4f1c3..9c2e01304 100644 --- a/backend/api/Services/ActionServices/CustomMissionSchedulingService.cs +++ b/backend/api/Services/ActionServices/CustomMissionSchedulingService.cs @@ -11,7 +11,7 @@ public interface ICustomMissionSchedulingService } public class CustomMissionSchedulingService( - ILogger logger, + ILogger logger, ICustomMissionService customMissionService, IAreaService areaService, ISourceService sourceService, diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index 023766f58..f38987ddf 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Linq.Dynamic.Core; using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; @@ -15,14 +16,10 @@ public interface IAreaService public Task> ReadByDeckId(string deckId); - public Task> ReadByInstallation(string installationCode); - public Task ReadByInstallationAndName(string installationCode, string areaName); public Task Create(CreateAreaQuery newArea); - public Task Create(CreateAreaQuery newArea, List safePositions); - public Task Update(Area area); public Task AddSafePosition(string installationCode, string areaName, SafePosition safePosition); @@ -58,11 +55,7 @@ public async Task> ReadAll() public async Task> ReadByDeckId(string deckId) { if (deckId == null) { return new List(); } - - return await context.Areas.Where(a => - a.Deck != null && a.Deck.Id.Equals(deckId) - ).Include(a => a.SafePositions).Include(a => a.Installation) - .Include(a => a.Plant).Include(a => a.Deck).ToListAsync(); + return await GetAreas().Where(a => a.Deck != null && a.Deck.Id.Equals(deckId)).ToListAsync(); } public async Task ReadByInstallationAndName(string installationCode, string areaName) @@ -70,11 +63,8 @@ public async Task> ReadAll() var installation = await installationService.ReadByName(installationCode); if (installation == null) { return null; } - return await context.Areas.Where(a => - a.Installation != null && a.Installation.Id.Equals(installation.Id) && - a.Name.ToLower().Equals(areaName.ToLower()) - ).Include(a => a.SafePositions).Include(a => a.Installation) - .Include(a => a.Plant).Include(a => a.Deck).FirstOrDefaultAsync(); + return await GetAreas().Where(a => + a.Installation.Id.Equals(installation.Id) && a.Name.ToLower().Equals(areaName.ToLower())).FirstOrDefaultAsync(); } public async Task> ReadByInstallation(string installationCode) @@ -82,9 +72,7 @@ public async Task> ReadByInstallation(string installationCode) var installation = await installationService.ReadByName(installationCode); if (installation == null) { return new List(); } - return await context.Areas.Where(a => - a.Installation != null && a.Installation.Id.Equals(installation.Id)).Include(a => a.SafePositions).Include(a => a.Installation) - .Include(a => a.Plant).Include(a => a.Deck).ToListAsync(); + return await GetAreas().Where(a => a.Installation.Id.Equals(installation.Id)).ToListAsync(); } public async Task Create(CreateAreaQuery newAreaQuery, List positions) @@ -191,27 +179,22 @@ private async Task ApplyDatabaseUpdate(Installation? installation) private IQueryable GetAreas() { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); - return context.Areas.Include(a => a.SafePositions) - .Include(a => a.Deck).Include(d => d.Plant).Include(i => i.Installation).Include(d => d.DefaultLocalizationPose) - .Where((a) => accessibleInstallationCodes.Result.Contains(a.Installation.InstallationCode.ToUpper())); - } - - public async Task ReadByInstallationAndName(Installation? installation, string areaName) - { - if (installation == null) { return null; } - - return await GetAreas().Where(a => - a.Name.ToLower().Equals(areaName.ToLower()) && - a.Installation.InstallationCode.Equals(installation.InstallationCode) - ).FirstOrDefaultAsync(); + return context.Areas + .Include(area => area.SafePositions) + .Include(area => area.DefaultLocalizationPose) + .Include(area => area.Deck) + .ThenInclude(deck => deck != null ? deck.DefaultLocalizationPose : null) + .Include(area => area.Plant) + .Include(area => area.Installation) + .Where((area) => accessibleInstallationCodes.Result.Contains(area.Installation.InstallationCode.ToUpper())); } public async Task ReadByInstallationAndPlantAndDeckAndName(Installation installation, Plant plant, Deck deck, string areaName) { return await GetAreas().Where(a => a.Deck != null && a.Deck.Id.Equals(deck.Id) && - a.Plant != null && a.Plant.Id.Equals(plant.Id) && - a.Installation != null && a.Installation.Id.Equals(installation.Id) && + a.Plant.Id.Equals(plant.Id) && + a.Installation.Id.Equals(installation.Id) && a.Name.ToLower().Equals(areaName.ToLower()) ).FirstOrDefaultAsync(); } diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs index 8e4c56236..d11aebd7b 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -92,8 +92,7 @@ private static List ProcessPlanItems(List planItems, string i { 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); } diff --git a/backend/api/Services/LocalizationService.cs b/backend/api/Services/LocalizationService.cs new file mode 100644 index 000000000..8e6f6f794 --- /dev/null +++ b/backend/api/Services/LocalizationService.cs @@ -0,0 +1,246 @@ +using System.Diagnostics; +using Api.Database.Models; +using Api.Utilities; +namespace Api.Services +{ + public interface ILocalizationService + { + public Task EnsureRobotIsCorrectlyLocalized(Robot robot, MissionRun missionRun); + + public Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition); + + public Task EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(string robotId); + + public Task RobotIsLocalized(string robotId); + } + + public class LocalizationService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService, IInstallationService installationService, IAreaService areaService) : ILocalizationService + { + + public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition) + { + var missionInstallation = await installationService.ReadByName(missionDefinition.InstallationCode); + + if (missionInstallation is null || robot.CurrentInstallation is null) + { + string errorMessage = $"Could not find installation for installation code {missionDefinition.InstallationCode} or the robot has no current installation"; + logger.LogError("{Message}", errorMessage); + throw new InstallationNotFoundException(errorMessage); + } + + if (robot.CurrentInstallation != missionInstallation) + { + 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); + } + } + + public async Task EnsureRobotIsCorrectlyLocalized(Robot robot, MissionRun missionRun) + { + if (missionRun.Area is null) + { + string errorMessage = $"There was no area associated with mission run {missionRun.Id}"; + logger.LogError("{Message}", errorMessage); + throw new AreaNotFoundException(errorMessage); + } + + string? localizationMissionRunId = null; + + if (!await RobotIsLocalized(robot.Id)) { localizationMissionRunId = await StartLocalizationMissionInArea(robot.Id, missionRun.Area.Id); } + + if (!await RobotIsOnSameDeckAsMission(robot.Id, missionRun.Area.Id)) + { + string errorMessage = $"The new mission run {missionRun.Id} will not be started as the robot is not localized on the same deck as the mission"; + logger.LogError("{Message}", errorMessage); + throw new RobotLocalizationException(errorMessage); + } + + logger.LogWarning("{Message}", $"Localization mission run ID is {localizationMissionRunId}"); + + return localizationMissionRunId; + } + + public async Task RobotIsLocalized(string robotId) + { + var robot = await robotService.ReadById(robotId); + if (robot is null) + { + string errorMessage = $"Robot with ID: {robotId} was not found in the database"; + logger.LogError("{Message}", errorMessage); + throw new RobotNotFoundException(errorMessage); + } + + return robot.CurrentArea is not null; + } + + public async Task EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(string robotId) + { + var robot = await robotService.ReadById(robotId); + if (robot == null) + { + string errorMessage = $"Robot with ID: {robotId} was not found in the database"; + logger.LogError("{Message}", errorMessage); + throw new RobotNotFoundException(errorMessage); + } + + if (await missionRunService.OngoingMission(robot.Id)) { await WaitForLocalizationMissionStatusToBeUpdated(robot); } + + var lastExecutedMissionRun = await missionRunService.ReadLastExecutedMissionRunByRobot(robot.Id); + if (lastExecutedMissionRun is null) + { + string errorMessage = $"Could not find last executed mission run for robot with ID {robot.Id}"; + logger.LogError("{Message}", errorMessage); + throw new MissionNotFoundException(errorMessage); + } + + if (lastExecutedMissionRun.Status != MissionStatus.Successful) + { + string errorMessage = + $"The localization mission {lastExecutedMissionRun.Id} failed and thus subsequent scheduled missions for deck {lastExecutedMissionRun.Area?.Deck} wil be cancelled"; + logger.LogError("{Message}", errorMessage); + throw new LocalizationFailedException(errorMessage); + } + + await robotService.UpdateCurrentArea(robot.Id, lastExecutedMissionRun.Area); + } + + private async Task WaitForLocalizationMissionStatusToBeUpdated(Robot robot) + { + if (robot.CurrentMissionId is null) + { + string errorMessage = $"Could not find current mission for robot {robot.Id}"; + logger.LogError("{Message}", errorMessage); + throw new MissionNotFoundException(errorMessage); + } + + string ongoingMissionRunId = robot.CurrentMissionId; + var ongoingMissionRun = await missionRunService.ReadById(robot.CurrentMissionId); + if (ongoingMissionRun is null) + { + string errorMessage = $"Could not find ongoing mission with ID {robot.CurrentMissionId}"; + logger.LogError("{Message}", errorMessage); + throw new MissionNotFoundException(errorMessage); + } + + if (!ongoingMissionRun.IsLocalizationMission()) + { + string errorMessage = $"The currently executing mission for robot {robot.CurrentMissionId} is not a localization mission"; + logger.LogError("{Message}", errorMessage); + throw new OngoingMissionNotLocalizationException(errorMessage); + } + + logger.LogWarning( + "The RobotAvailable event was triggered before the OnMissionUpdate event and we have to wait to see that the localization mission is set to successful"); + + const int Timeout = 5; + var timer = new Stopwatch(); + ongoingMissionRun = await missionRunService.ReadById(ongoingMissionRunId); + + timer.Start(); + while (timer.Elapsed.TotalSeconds < Timeout) + { + if (ongoingMissionRun is null) { continue; } + if (ongoingMissionRun.Status == MissionStatus.Successful) { return; } + + ongoingMissionRun = await missionRunService.ReadById(ongoingMissionRunId); + } + + const string Message = "Timed out while waiting for the localization mission to get an updated status"; + logger.LogError("{Message}", Message); + throw new TimeoutException(Message); + } + + private async Task StartLocalizationMissionInArea(string robotId, string areaId) + { + var robot = await robotService.ReadById(robotId); + if (robot is null) + { + string errorMessage = $"The robot with ID {robotId} was not found"; + logger.LogError("{Message}", errorMessage); + throw new RobotNotFoundException(errorMessage); + } + + var area = await areaService.ReadById(areaId); + if (area is null) + { + string errorMessage = $"The area with ID {areaId} was not found"; + logger.LogError("{Message}", errorMessage); + throw new AreaNotFoundException(errorMessage); + } + + if (area.Deck?.DefaultLocalizationPose?.Pose is null) + { + const string ErrorMessage = "The mission area is not associated with any deck or that deck does not have a localization pose"; + logger.LogError("{Message}", ErrorMessage); + throw new DeckNotFoundException(ErrorMessage); + } + if (robot.Status is not RobotStatus.Available) + { + string errorMessage = $"Robot '{robot.Id}' is not available as the status is {robot.Status}"; + logger.LogWarning("{Message}", errorMessage); + throw new RobotNotAvailableException(errorMessage); + } + + var localizationMissionRun = new MissionRun + { + Name = "Localization mission", + Robot = robot, + InstallationCode = area.Installation.InstallationCode, + Area = area, + Status = MissionStatus.Pending, + DesiredStartTime = DateTime.UtcNow, + Tasks = new List + { + new(area.Deck.DefaultLocalizationPose.Pose, MissionTaskType.Localization) + }, + Map = new MapMetadata() + }; + logger.LogWarning("Starting localization mission"); + await missionRunService.Create(localizationMissionRun, triggerCreatedMissionRunEvent: false); + await robotService.UpdateCurrentArea(robot.Id, localizationMissionRun.Area); + return localizationMissionRun.Id; + } + + private async Task RobotIsOnSameDeckAsMission(string robotId, string areaId) + { + var robot = await robotService.ReadById(robotId); + if (robot is null) + { + string errorMessage = $"The robot with ID {robotId} was not found"; + logger.LogError("{Message}", errorMessage); + throw new RobotNotFoundException(errorMessage); + } + + if (robot.CurrentArea is null) + { + const string ErrorMessage = "The robot is not associated with an area and a mission may not be started"; + logger.LogError("{Message}", ErrorMessage); + throw new AreaNotFoundException(ErrorMessage); + } + + var missionArea = await areaService.ReadById(areaId); + if (missionArea is null) + { + const string ErrorMessage = "The robot is not located on the same deck as the mission as the area has not been set"; + logger.LogError("{Message}", ErrorMessage); + throw new AreaNotFoundException(ErrorMessage); + } + + if (robot.CurrentArea?.Deck is null) + { + const string ErrorMessage = "The robot area is not associated with any deck"; + logger.LogError("{Message}", ErrorMessage); + throw new DeckNotFoundException(ErrorMessage); + } + if (missionArea.Deck is null) + { + const string ErrorMessage = "The mission area is not associated with any deck"; + logger.LogError("{Message}", ErrorMessage); + throw new DeckNotFoundException(ErrorMessage); + } + + return robot.CurrentArea.Deck == missionArea.Deck; + } + } +} diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index 33324bbb1..52bb93c64 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -44,8 +44,8 @@ public class MissionDefinitionService(FlotillaDbContext context, IStidService stidService, ICustomMissionService customMissionService, ISignalRService signalRService, - ILogger logger, - IAccessRoleService accessRoleService) : IMissionDefinitionService + IAccessRoleService accessRoleService, + ILogger logger) : IMissionDefinitionService { public async Task Create(MissionDefinition missionDefinition) { @@ -164,6 +164,7 @@ private async Task ApplyDatabaseUpdate(Installation? installation) await context.SaveChangesAsync(); else throw new UnauthorizedAccessException($"User does not have permission to update mission definition in installation {installation.Name}"); + } private IQueryable GetMissionDefinitionsWithSubModels() @@ -173,20 +174,22 @@ private IQueryable GetMissionDefinitionsWithSubModels() .Include(missionDefinition => missionDefinition.Area != null ? missionDefinition.Area.Deck : null) .ThenInclude(deck => deck != null ? deck.Plant : null) .ThenInclude(plant => plant != null ? plant.Installation : null) + .Include(missionDefinition => missionDefinition.Area) .Include(missionDefinition => missionDefinition.Source) .Include(missionDefinition => missionDefinition.LastSuccessfulRun) .ThenInclude(missionRun => missionRun != null ? missionRun.Tasks : null)! .ThenInclude(missionTask => missionTask.Inspections) .ThenInclude(inspection => inspection.InspectionFindings) + .Include(missionDefinition => missionDefinition.Area != null ? missionDefinition.Area.Deck : null) + .ThenInclude(deck => deck != null ? deck.DefaultLocalizationPose : null) + .ThenInclude(defaultLocalizationPose => defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null) .Where((m) => m.Area == null || accessibleInstallationCodes.Result.Contains(m.Area.Installation.InstallationCode.ToUpper())); } private static void SearchByName(ref IQueryable missionDefinitions, string? name) { if (!missionDefinitions.Any() || string.IsNullOrWhiteSpace(name)) - { return; - } missionDefinitions = missionDefinitions.Where( missionDefinition => diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index d2cf078ca..3f3754f9c 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -11,7 +11,7 @@ namespace Api.Services { public interface IMissionRunService { - public Task Create(MissionRun missionRun); + public Task Create(MissionRun missionRun, bool triggerCreatedMissionRunEvent = true); public Task> ReadAll(MissionRunQueryStringParameters parameters); @@ -27,6 +27,8 @@ public interface IMissionRunService public Task ReadNextScheduledEmergencyMissionRun(string robotId); + public Task ReadLastExecutedMissionRunByRobot(string robotId); + public Task Update(MissionRun mission); public Task UpdateMissionRunStatusByIsarMissionId( @@ -35,6 +37,8 @@ MissionStatus missionStatus ); public Task Delete(string id); + + public Task OngoingMission(string robotId); } [SuppressMessage( @@ -53,7 +57,7 @@ public class MissionRunService( ILogger logger, IAccessRoleService accessRoleService) : IMissionRunService { - public async Task Create(MissionRun missionRun) + public async Task Create(MissionRun missionRun, bool triggerCreatedMissionRunEvent = true) { missionRun.Id ??= Guid.NewGuid().ToString(); // Useful for signalR messages // Making sure database does not try to create new robot @@ -62,10 +66,13 @@ public async Task Create(MissionRun missionRun) await context.MissionRuns.AddAsync(missionRun); await ApplyDatabaseUpdate(missionRun.Area?.Installation); - _ = signalRService.SendMessageAsync("Mission run created", missionRun?.Area?.Installation, missionRun); + _ = signalRService.SendMessageAsync("Mission run created", missionRun.Area?.Installation, missionRun); - var args = new MissionRunCreatedEventArgs(missionRun!.Id); - OnMissionRunCreated(args); + if (triggerCreatedMissionRunEvent) + { + var args = new MissionRunCreatedEventArgs(missionRun.Id); + OnMissionRunCreated(args); + } return missionRun; } @@ -121,12 +128,22 @@ public async Task> ReadMissionRunQueue(string robotId) public async Task ReadNextScheduledRunByMissionId(string missionId) { + var test = GetMissionRunsWithSubModels().OrderBy(m => m.DesiredStartTime).ToList(); + return await GetMissionRunsWithSubModels() .Where(m => m.MissionId == missionId && m.EndTime == null) .OrderBy(m => m.DesiredStartTime) .FirstOrDefaultAsync(); } + public async Task ReadLastExecutedMissionRunByRobot(string robotId) + { + return await GetMissionRunsWithSubModels() + .Where(m => m.Robot.Id == robotId) + .OrderByDescending(m => m.EndTime) + .FirstOrDefaultAsync(); + } + public async Task Update(MissionRun missionRun) { context.Entry(missionRun.Robot).State = EntityState.Unchanged; @@ -154,16 +171,34 @@ public async Task Update(MissionRun missionRun) return missionRun; } + public async Task OngoingMission(string robotId) + { + var ongoingMissions = await ReadAll( + new MissionRunQueryStringParameters + { + Statuses = [MissionStatus.Ongoing], + RobotId = robotId, + OrderBy = "DesiredStartTime", + PageSize = 100 + }); + + return ongoingMissions.Any(); + } + private IQueryable GetMissionRunsWithSubModels() { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); return context.MissionRuns .Include(missionRun => missionRun.Area) .ThenInclude(area => area != null ? area.Deck : null) - .Include(missionRun => missionRun.Area) - .ThenInclude(area => area != null ? area.Plant : null) + .ThenInclude(deck => deck != null ? deck.Plant : null) + .ThenInclude(plant => plant != null ? plant.Installation : null) .Include(missionRun => missionRun.Area) .ThenInclude(area => area != null ? area.Installation : null) + .Include(missionRun => missionRun.Area) + .ThenInclude(area => area != null ? area.Deck : null) + .ThenInclude(deck => deck != null ? deck.DefaultLocalizationPose : null) + .ThenInclude(defaultLocalizationPose => defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null) .Include(missionRun => missionRun.Robot) .ThenInclude(robot => robot.VideoStreams) .Include(missionRun => missionRun.Robot) diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index 0e9ac4102..9ec6ff33b 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -17,6 +17,8 @@ public interface IMissionSchedulingService public Task StopCurrentMissionRun(string robotId); + public Task CancelAllScheduledMissions(string robotId); + public Task ScheduleMissionToReturnToSafePosition(string robotId, string areaId); public Task UnfreezeMissionRunQueueForRobot(string robotId); @@ -128,6 +130,33 @@ public async Task StopCurrentMissionRun(string robotId) catch (RobotNotFoundException) { } } + public async Task CancelAllScheduledMissions(string robotId) + { + var robot = await robotService.ReadById(robotId); + if (robot == null) + { + string errorMessage = $"Robot with ID: {robotId} was not found in the database"; + logger.LogError("{Message}", errorMessage); + throw new RobotNotFoundException(errorMessage); + } + + var pendingMissionRuns = await missionRunService.ReadMissionRunQueue(robotId); + if (pendingMissionRuns is null) + { + string infoMessage = $"There were no mission runs in the queue to stop for robot {robotId}"; + logger.LogWarning("{Message}", infoMessage); + return; + } + + IList pendingMissionRunIds = pendingMissionRuns.Select(missionRun => missionRun.Id).ToList(); + + foreach (var pendingMissionRun in pendingMissionRuns) + { + pendingMissionRun.Status = MissionStatus.Cancelled; + await missionRunService.Update(pendingMissionRun); + } + } + public async Task ScheduleMissionToReturnToSafePosition(string robotId, string areaId) { var area = await areaService.ReadById(areaId); @@ -181,6 +210,7 @@ public void TriggerRobotAvailable(RobotAvailableEventArgs e) { OnRobotAvailable(e); } + private async Task MoveInterruptedMissionsToQueue(IEnumerable interruptedMissionRunIds) { foreach (string missionRunId in interruptedMissionRunIds) @@ -346,6 +376,7 @@ private static float CalculateDistance(Pose pose1, Pose pose2) var pos2 = pose2.Position; return (float)Math.Sqrt(Math.Pow(pos1.X - pos2.X, 2) + Math.Pow(pos1.Y - pos2.Y, 2) + Math.Pow(pos1.Z - pos2.Z, 2)); } + protected virtual void OnRobotAvailable(RobotAvailableEventArgs e) { RobotAvailable?.Invoke(this, e); } public static event EventHandler? RobotAvailable; } diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index 522cf057f..34eac19d6 100644 --- a/backend/api/Services/Models/IsarMissionDefinition.cs +++ b/backend/api/Services/Models/IsarMissionDefinition.cs @@ -37,6 +37,9 @@ public struct IsarTaskDefinition [JsonPropertyName("id")] public string? Id { get; set; } + [JsonPropertyName("type")] + public string Type { get; set; } + [JsonPropertyName("pose")] public IsarPose Pose { get; set; } @@ -49,6 +52,7 @@ public struct IsarTaskDefinition public IsarTaskDefinition(MissionTask missionTask, MissionRun missionRun) { Id = missionTask.IsarTaskId; + Type = MissionTask.ConvertMissionTaskTypeToIsarTaskType(missionTask.Type); Pose = new IsarPose(missionTask.RobotPose); Tag = missionTask.TagId; var isarInspections = new List(); diff --git a/backend/api/Services/ReturnToHomeService.cs b/backend/api/Services/ReturnToHomeService.cs new file mode 100644 index 000000000..4e72432b0 --- /dev/null +++ b/backend/api/Services/ReturnToHomeService.cs @@ -0,0 +1,78 @@ +using Api.Database.Models; +using Api.Utilities; +namespace Api.Services +{ + public interface IReturnToHomeService + { + public Task ScheduleReturnToHomeMissionRun(string robotId); + } + + public class ReturnToHomeService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService) : IReturnToHomeService + { + public async Task ScheduleReturnToHomeMissionRun(string robotId) + { + var robot = await robotService.ReadById(robotId); + if (robot is null) + { + string errorMessage = $"Robot with ID {robotId} could not be retrieved from the database"; + logger.LogError("{Message}", errorMessage); + throw new RobotNotFoundException(errorMessage); + } + + if (robot.CurrentInstallation is null) + { + string errorMessage = $"Unable to schedule a return to home mission as the robot {robot.Id} is not linked to an installation"; + logger.LogError("{Message}", errorMessage); + throw new InstallationNotFoundException(errorMessage); + } + + if (robot.CurrentArea is null) + { + string errorMessage = $"Unable to schedule a return to home mission as the robot {robot.Id} is not linked to an area"; + logger.LogError("{Message}", errorMessage); + throw new AreaNotFoundException(errorMessage); + } + + if (robot.CurrentArea.Deck is null) + { + string errorMessage = $"Unable to schedule a return to home mission as the current area {robot.CurrentArea.Id} for robot {robot.Id} is not linked to a deck"; + logger.LogError("{Message}", errorMessage); + throw new DeckNotFoundException(errorMessage); + } + + if (robot.CurrentArea.Deck.DefaultLocalizationPose is null) + { + logger.LogError( + "Unable to schedule a return to home mission as the current area {AreaId} for robot {RobotId} is linked to the deck {DeckId} which has no default pose", + robot.CurrentArea.Id, robot.Id, robot.CurrentArea.Deck.Id); + string errorMessage = + $"Unable to schedule a return to home mission as the current area {robot.CurrentArea.Id} for robot {robot.Id} " + + $"is linked to the deck {robot.CurrentArea.Deck.Id} which has no default pose"; + logger.LogError("{Message}", errorMessage); + throw new PoseNotFoundException(errorMessage); + } + + var returnToHomeMissionRun = new MissionRun + { + Name = "Return to home mission", + Robot = robot, + InstallationCode = robot.CurrentInstallation.InstallationCode, + MissionRunPriority = MissionRunPriority.Normal, + Area = robot.CurrentArea, + Status = MissionStatus.Pending, + DesiredStartTime = DateTime.UtcNow, + Tasks = new List + { + new(robot.CurrentArea.Deck.DefaultLocalizationPose.Pose, MissionTaskType.DriveTo) + }, + Map = new MapMetadata() + }; + + var missionRun = await missionRunService.Create(returnToHomeMissionRun); + logger.LogInformation( + "Scheduled a mission for the robot {RobotName} to return to home location on deck {DeckName}", + robot.Name, robot.CurrentArea.Deck.Name); + return missionRun; + } + } +} diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index 592167c5e..558790175 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -38,7 +38,8 @@ public class RobotService(FlotillaDbContext context, IRobotModelService robotModelService, ISignalRService signalRService, IAccessRoleService accessRoleService, - IInstallationService installationService) : IRobotService, IDisposable + IInstallationService installationService, + IAreaService areaService) : IRobotService, IDisposable { private readonly Semaphore _robotSemaphore = new(1, 1); @@ -69,7 +70,18 @@ public async Task CreateFromQuery(CreateRobotQuery robotQuery) throw new DbUpdateException($"Could not create new robot in database as installation {robotQuery.CurrentInstallationCode} doesn't exist"); } - var newRobot = new Robot(robotQuery, installation) + Area? area = null; + if (robotQuery.CurrentAreaName is not null) + { + area = await areaService.ReadByInstallationAndName(robotQuery.CurrentInstallationCode, robotQuery.CurrentAreaName); + if (area is null) + { + logger.LogError("Area '{AreaName}' does not exist in installation {CurrentInstallation}", robotQuery.CurrentAreaName, robotQuery.CurrentInstallationCode); + throw new DbUpdateException($"Could not create new robot in database as area '{robotQuery.CurrentAreaName}' does not exist in installation {robotQuery.CurrentInstallationCode}"); + } + } + + var newRobot = new Robot(robotQuery, installation, area) { Model = robotModel }; @@ -172,7 +184,6 @@ private async Task UpdateRobotProperty(string robotId, string propertyNam private IQueryable GetRobotsWithSubModels() { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); -#pragma warning disable CA1304 return context.Robots .Include(r => r.VideoStreams) .Include(r => r.Model) @@ -185,6 +196,11 @@ private IQueryable GetRobotsWithSubModels() .ThenInclude(area => area != null ? area.Installation : null) .Include(r => r.CurrentArea) .ThenInclude(area => area != null ? area.SafePositions : null) + .Include(r => r.CurrentArea) + .ThenInclude(area => area != null ? area.Deck : null) + .ThenInclude(deck => deck != null ? deck.DefaultLocalizationPose : null) + .ThenInclude(defaultLocalizationPose => defaultLocalizationPose != null ? defaultLocalizationPose.Pose : null) +#pragma warning disable CA1304 .Where((r) => r.CurrentInstallation == null || r.CurrentInstallation.InstallationCode == null || accessibleInstallationCodes.Result.Contains(r.CurrentInstallation.InstallationCode.ToUpper())); #pragma warning restore CA1304 } diff --git a/backend/api/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs index cde89a937..54e85385a 100644 --- a/backend/api/Utilities/Exceptions.cs +++ b/backend/api/Utilities/Exceptions.cs @@ -18,6 +18,9 @@ public MissionException(string message, int isarStatusCode) : base(message) public class MissionSourceTypeException(string message) : Exception(message) { } + public class OngoingMissionNotLocalizationException(string message) : Exception(message) + { + } public class SourceException(string message) : Exception(message) { @@ -82,4 +85,28 @@ public class DeckExistsException(string message) : Exception(message) public class SafeZoneException(string message) : Exception(message) { } + + public class RobotNotAvailableException(string message) : Exception(message) + { + } + + public class RobotNotInSameInstallationAsMissionException(string message) : Exception(message) + { + } + + public class PoseNotFoundException(string message) : Exception(message) + { + } + + public class RobotLocalizationException(string message) : Exception(message) + { + } + + public class IsarCommunicationException(string message) : Exception(message) + { + } + + public class LocalizationFailedException(string message) : Exception(message) + { + } }