From 6b0debee1f4aa0c430d0e8eaae5a76648c6cfe2c Mon Sep 17 00:00:00 2001 From: aestene Date: Fri, 13 Oct 2023 14:12:18 +0200 Subject: [PATCH 01/18] Use robot pose directly from Echo request --- backend/api/Services/EchoService.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs index 8e4c56236..a347ab93c 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -139,9 +139,7 @@ private List ProcessAvailableEchoMission(List echoPlantInfoResponse var echoPlantInfo = new EchoPlantInfo { - PlantCode = plant.InstallationCode, - ProjectDescription = plant.ProjectDescription + PlantCode = plant.InstallationCode, ProjectDescription = plant.ProjectDescription }; echoPlantInfos.Add(echoPlantInfo); From e7e74043807429e3c4f9f4129283b5bc6d914d77 Mon Sep 17 00:00:00 2001 From: aestene Date: Wed, 18 Oct 2023 11:12:03 +0200 Subject: [PATCH 02/18] Fix formatting --- backend/api/Services/EchoService.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs index a347ab93c..8e4c56236 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -139,7 +139,9 @@ private List ProcessAvailableEchoMission(List echoPlantInfoResponse var echoPlantInfo = new EchoPlantInfo { - PlantCode = plant.InstallationCode, ProjectDescription = plant.ProjectDescription + PlantCode = plant.InstallationCode, + ProjectDescription = plant.ProjectDescription }; echoPlantInfos.Add(echoPlantInfo); From 9def051682cf0f272497c997d83d682f756f57b9 Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 10 Oct 2023 12:53:35 +0200 Subject: [PATCH 03/18] Implement localization as part of a mission The localization procedure is now implemented and will run as part of regular mission scheduling. If the queue is empty a localization mission will be started for the current deck. This assumes that the operator has confirmed that the robot is on the deck of the mission that has been scheduled. If there is an existing mission the system will check if a new mission is in the same deck as that mission and if so schedule it. If not it will be rejected. If the last mission finishes a return to home mission will be scheduled which puts the robot back at the default localization pose. If a mission is scheduled in between the return to home mission another localization will not be required. --- .../Mocks/CustomMissionServiceMock.cs | 16 +- .../MissionSchedulingController.cs | 45 +- backend/api/Database/Context/InitDb.cs | 33 +- backend/api/Database/Models/MissionRun.cs | 20 +- backend/api/Database/Models/MissionTask.cs | 29 + .../api/EventHandlers/MissionEventHandler.cs | 36 +- ...018095954_AddTypeToMissionTask.Designer.cs | 1260 +++++++++++++++++ .../20231018095954_AddTypeToMissionTask.cs | 29 + backend/api/Program.cs | 6 +- .../CustomMissionSchedulingService.cs | 2 +- backend/api/Services/AreaService.cs | 26 +- backend/api/Services/CustomMissionService.cs | 1 + backend/api/Services/LocalizationService.cs | 230 +++ .../api/Services/MissionDefinitionService.cs | 64 +- backend/api/Services/MissionRunService.cs | 29 + .../Services/Models/IsarMissionDefinition.cs | 4 + backend/api/Services/ReturnToHomeService.cs | 81 ++ backend/api/Services/RobotService.cs | 5 +- backend/api/Utilities/Exceptions.cs | 21 + 19 files changed, 1883 insertions(+), 54 deletions(-) create mode 100644 backend/api/Migrations/20231018095954_AddTypeToMissionTask.Designer.cs create mode 100644 backend/api/Migrations/20231018095954_AddTypeToMissionTask.cs create mode 100644 backend/api/Services/LocalizationService.cs create mode 100644 backend/api/Services/ReturnToHomeService.cs diff --git a/backend/api.test/Mocks/CustomMissionServiceMock.cs b/backend/api.test/Mocks/CustomMissionServiceMock.cs index 342da6740..89c1b3d72 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,17 @@ 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(); } + + public async Task QueueCustomMissionRun(CustomMissionQuery customMissionQuery, MissionDefinition customMissionDefinition, Robot robot, IList missionTasks) + { + await Task.CompletedTask; + return new MissionRun(); + } } } diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index 6b7ade28e..e24a62612 100644 --- a/backend/api/Controllers/MissionSchedulingController.cs +++ b/backend/api/Controllers/MissionSchedulingController.cs @@ -19,10 +19,14 @@ public class MissionSchedulingController( ILogger logger, IMapService mapService, IStidService stidService, + ILocalizationService localizationService, ISourceService sourceService, ICustomMissionSchedulingService customMissionSchedulingService ) : ControllerBase { + + private readonly Mutex _scheduleLocalizationMutex = new(); + /// /// Schedule an existing mission definition /// @@ -42,19 +46,25 @@ [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"); } + + try { await localizationService.EnsureRobotIsOnSameInstallationAsMission(robot, missionDefinition); } + catch (InstallationNotFoundException e) { return NotFound(e.Message); } + catch (MissionException e) { return Conflict(e.Message); } + + var missionTasks = await missionDefinitionService.GetTasksFromSource(missionDefinition.Source, missionDefinition.InstallationCode); - List? missionTasks; - missionTasks = await missionDefinitionService.GetTasksFromSource(missionDefinition.Source, missionDefinition.InstallationCode); + _scheduleLocalizationMutex.WaitOne(); + + try { await localizationService.EnsureRobotIsCorrectlyLocalized(robot, missionDefinition); } + catch (Exception e) when (e is AreaNotFoundException or DeckNotFoundException) { return NotFound(e.Message); } + catch (Exception e) when (e is RobotNotAvailableException or RobotLocalizationException) { return Conflict(e.Message); } + catch (IsarCommunicationException e) { return StatusCode(StatusCodes.Status502BadGateway, e.Message); } + + finally { _scheduleLocalizationMutex.ReleaseMutex(); } if (missionTasks == null) { @@ -72,7 +82,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 +280,19 @@ [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 (MissionException e) { return Conflict(e.Message); } + + _scheduleLocalizationMutex.WaitOne(); + + try { await localizationService.EnsureRobotIsCorrectlyLocalized(robot, customMissionDefinition); } + catch (Exception e) when (e is AreaNotFoundException or DeckNotFoundException) { return NotFound(e.Message); } + catch (Exception e) when (e is RobotNotAvailableException or RobotLocalizationException) { return Conflict(e.Message); } + catch (IsarCommunicationException e) { return StatusCode(StatusCodes.Status502BadGateway, e.Message); } + + finally { _scheduleLocalizationMutex.ReleaseMutex(); } + 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/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index ba624e165..be32ee4a8 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 = "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 }); } @@ -179,10 +193,7 @@ private static List GetAreas() Name = "testArea", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List - { - new() - } + SafePositions = new List() }; var area4 = 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..ce978311b 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -139,7 +139,7 @@ public void CalculateEstimatedDuration() task => task.Inspections.Sum(inspection => inspection.VideoDuration ?? 0) ); EstimatedDuration = (uint)( - (Robot.Model.AverageDurationPerTag * Tasks.Count) + totalInspectionDuration + Robot.Model.AverageDurationPerTag * Tasks.Count + totalInspectionDuration ); } else @@ -188,6 +188,24 @@ var inspection in task.Inspections.Where(inspection => !inspection.IsCompleted) } } } + + public bool IsLocalizationMission() + { + if (Tasks.Count != 1) + { + return false; + } + return Tasks[0].Type == "localization"; + } + + public bool IsDriveToMission() + { + if (Tasks.Count != 1) + { + return false; + } + return Tasks[0].Type == "drive_to"; + } } public enum MissionStatus diff --git a/backend/api/Database/Models/MissionTask.cs b/backend/api/Database/Models/MissionTask.cs index 996430348..1f80da322 100644 --- a/backend/api/Database/Models/MissionTask.cs +++ b/backend/api/Database/Models/MissionTask.cs @@ -26,6 +26,7 @@ public MissionTask(EchoTag echoTag, Position tagPosition) EchoPoseId = echoTag.PoseId; TaskOrder = echoTag.PlanOrder; Status = TaskStatus.NotStarted; + Type = "inspection"; } // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized @@ -40,6 +41,32 @@ public MissionTask(CustomTaskQuery taskQuery) RobotPose = taskQuery.RobotPose; TaskOrder = taskQuery.TaskOrder; Status = TaskStatus.NotStarted; + Type = "inspection"; + } + + public MissionTask(Pose robotPose, string type) + { + switch (type) + { + case "localization": + Type = type; + Description = "Localization"; + RobotPose = robotPose; + TaskOrder = 0; + Status = TaskStatus.NotStarted; + InspectionTarget = new Position(); + Inspections = new List(); + break; + case "drive_to": + Type = type; + Description = "Return to home"; + RobotPose = robotPose; + TaskOrder = 0; + Status = TaskStatus.NotStarted; + InspectionTarget = new Position(); + Inspections = new List(); + break; + } } // Creates a blank deepcopy of the provided task @@ -68,6 +95,8 @@ public MissionTask(MissionTask copy) [Required] public int TaskOrder { get; set; } + public string Type { get; set; } + [MaxLength(200)] public string? TagId { get; set; } diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 7690f2629..bcc103e15 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -23,12 +23,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(); @@ -66,6 +69,12 @@ private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArg return; } + if (!await LocalizationService.RobotIsLocalized(missionRun.Robot.Id) && !missionRun.IsLocalizationMission()) + { + _logger.LogWarning("A mission run was created while the robot was not localized and it will be put on the queue awaiting localization"); + return; + } + 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); @@ -87,9 +96,34 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) return; } + if (!await LocalizationService.RobotIsLocalized(robot.Id)) + { + try { await LocalizationService.EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(robot.Id); } + catch (Exception ex) when (ex is LocalizationFailedException or RobotNotFoundException or MissionNotFoundException or MissionException or TimeoutException) + { + _logger.LogError("Could not confirm that the robot was correctly localized and the scheduled missions for the deck will be cancelled"); + // Cancel missions + // Raise localization mission failed event? + } + } + 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) { return; } + } + else { await RobotService.UpdateCurrentArea(robot.Id, null); } return; } 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..5a27cc8f1 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -70,11 +70,13 @@ 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 context.Areas.Where(a => 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) + .ThenInclude(d => d != null ? d.DefaultLocalizationPose : null) + .FirstOrDefaultAsync(); } public async Task> ReadByInstallation(string installationCode) @@ -83,7 +85,7 @@ public async Task> ReadByInstallation(string 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) + a.Installation.Id.Equals(installation.Id)).Include(a => a.SafePositions).Include(a => a.Installation) .Include(a => a.Plant).Include(a => a.Deck).ToListAsync(); } @@ -200,18 +202,18 @@ private IQueryable GetAreas() { if (installation == null) { return null; } - return await GetAreas().Where(a => - a.Name.ToLower().Equals(areaName.ToLower()) && - a.Installation.InstallationCode.Equals(installation.InstallationCode) - ).FirstOrDefaultAsync(); + return await context.Areas.Where(a => + a.Name.ToLower().Equals(areaName.ToLower()) && a.Installation.Id.Equals(installation.Id) + ).Include(a => a.SafePositions).Include(a => a.Installation) + .Include(a => a.Plant).Include(a => a.Deck).FirstOrDefaultAsync(); } 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/CustomMissionService.cs b/backend/api/Services/CustomMissionService.cs index d19ca83b3..51060d680 100644 --- a/backend/api/Services/CustomMissionService.cs +++ b/backend/api/Services/CustomMissionService.cs @@ -1,6 +1,7 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; +using Api.Controllers.Models; using Api.Database.Models; using Api.Options; using Microsoft.Extensions.Options; diff --git a/backend/api/Services/LocalizationService.cs b/backend/api/Services/LocalizationService.cs new file mode 100644 index 000000000..156c2f033 --- /dev/null +++ b/backend/api/Services/LocalizationService.cs @@ -0,0 +1,230 @@ +using System.Diagnostics; +using Api.Database.Models; +using Api.Utilities; +namespace Api.Services +{ + public interface ILocalizationService + { + public Task EnsureRobotIsCorrectlyLocalized(Robot robot, MissionDefinition missionDefinition); + + public Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition); + + public Task EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(string robotId); + + public Task RobotIsLocalized(string robotId); + } + + public class LocalizationService : ILocalizationService + { + private readonly IInstallationService _installationService; + private readonly ILogger _logger; + private readonly IMissionRunService _missionRunService; + private readonly IRobotService _robotService; + + public LocalizationService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService, IInstallationService installationService) + { + _logger = logger; + _robotService = robotService; + _missionRunService = missionRunService; + _installationService = installationService; + } + + public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition) + { + var missionInstallation = await _installationService.ReadByName(missionDefinition.InstallationCode); + var robotInstallation = await _installationService.ReadByName(robot.CurrentInstallation); + + if (missionInstallation is null || robotInstallation is null) + { + string errorMessage = $"Could not find installation for installation code {missionDefinition.InstallationCode}"; + _logger.LogError("{Message}", errorMessage); + throw new InstallationNotFoundException(errorMessage); + } + + if (robotInstallation != missionInstallation) + { + string errorMessage = $"The robot {robot.Name} is on installation {robotInstallation.Name} which is not the same as the mission installation {missionInstallation.Name}"; + _logger.LogError("{Message}", errorMessage); + throw new MissionException(errorMessage); + } + } + + public async Task EnsureRobotIsCorrectlyLocalized(Robot robot, MissionDefinition missionDefinition) + { + if (missionDefinition.Area is null) + { + string errorMessage = $"There was no area associated with mission definition {missionDefinition.Id}"; + _logger.LogError("{Message}", errorMessage); + throw new AreaNotFoundException(errorMessage); + } + + if (!await RobotIsLocalized(robot.Id)) { await StartLocalizationMissionInArea(robot, missionDefinition.Area); } + + if (!RobotIsOnSameDeckAsMission(robot.CurrentArea, missionDefinition.Area)) + { + string errorMessage = $"A new mission run of mission definition {missionDefinition.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); + } + } + + public async Task RobotIsLocalized(string robotId) + { + var robot = await _robotService.ReadById(robotId); + if (robot != null) + { + return robot.CurrentArea is not null; + } + + string errorMessage = $"Robot with ID: {robotId} was not found in the database"; + _logger.LogError("{Message}", errorMessage); + throw new RobotNotFoundException(errorMessage); + } + + 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.SetCurrentArea(lastExecutedMissionRun.Area, robot.Id); + } + + 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 MissionException(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) { break; } + + 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(Robot robot, Area missionDefinitionArea) + { + if (missionDefinitionArea.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.ToString()}"; + _logger.LogWarning("{Message}", errorMessage); + throw new RobotNotAvailableException(errorMessage); + } + + var localizationMissionRun = new MissionRun + { + Name = "Localization mission", + Robot = robot, + InstallationCode = missionDefinitionArea.Installation.InstallationCode, + Area = missionDefinitionArea, + Status = MissionStatus.Pending, + DesiredStartTime = DateTime.UtcNow, + Tasks = new List + { + new(missionDefinitionArea.Deck.DefaultLocalizationPose.Pose, "localization") + }, + Map = new MapMetadata() + }; + + await _missionRunService.Create(localizationMissionRun); + + robot.CurrentArea = localizationMissionRun.Area; + await _robotService.Update(robot); + } + + private bool RobotIsOnSameDeckAsMission(Area? currentRobotArea, Area? missionArea) + { + if (currentRobotArea 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); + } + 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 (currentRobotArea?.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 currentRobotArea.Deck == missionArea.Deck; + } + } +} diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index 33324bbb1..b2b3cace6 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -24,6 +24,8 @@ public interface IMissionDefinitionService public Task> ReadBySourceId(string sourceId); + public Task FindExistingOrCreateCustomMissionDefinition(CustomMissionQuery customMissionQuery, List missionTasks); + public Task Update(MissionDefinition missionDefinition); public Task Delete(string id); @@ -40,12 +42,14 @@ public interface IMissionDefinitionService Justification = "Entity framework does not support translating culture info to SQL calls" )] public class MissionDefinitionService(FlotillaDbContext context, + IAreaService areaService, IEchoService echoService, IStidService stidService, ICustomMissionService customMissionService, ISignalRService signalRService, - ILogger logger, - IAccessRoleService accessRoleService) : IMissionDefinitionService + ISourceService sourceService, + IAccessRoleService accessRoleService, + ILogger logger) : IMissionDefinitionService { public async Task Create(MissionDefinition missionDefinition) { @@ -164,6 +168,56 @@ 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}"); + + } + + public async Task FindExistingOrCreateCustomMissionDefinition(CustomMissionQuery customMissionQuery, List missionTasks) + { + Area? area = null; + if (customMissionQuery.AreaName != null) { area = await areaService.ReadByInstallationAndName(customMissionQuery.InstallationCode, customMissionQuery.AreaName); } + + var source = await sourceService.CheckForExistingCustomSource(missionTasks); + + MissionDefinition? existingMissionDefinition = null; + if (source == null) + { + try + { + string sourceUrl = await customMissionService.UploadSource(missionTasks); + source = new Source + { + SourceId = sourceUrl, + Type = MissionSourceType.Custom + }; + } + catch (Exception e) + { + { + string errorMessage = $"Unable to upload source for mission {customMissionQuery.Name}"; + logger.LogError(e, "{Message}", errorMessage); + throw new SourceException(errorMessage); + } + } + } + else + { + var missionDefinitions = await ReadBySourceId(source.SourceId); + if (missionDefinitions.Count > 0) { existingMissionDefinition = missionDefinitions.First(); } + } + + var customMissionDefinition = existingMissionDefinition ?? new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Source = source, + Name = customMissionQuery.Name, + InspectionFrequency = customMissionQuery.InspectionFrequency, + InstallationCode = customMissionQuery.InstallationCode, + Area = area + }; + + if (existingMissionDefinition == null) { await Create(customMissionDefinition); } + + return customMissionDefinition; } private IQueryable GetMissionDefinitionsWithSubModels() @@ -173,20 +227,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..7a8c8b837 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -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( @@ -127,6 +131,14 @@ public async Task> ReadMissionRunQueue(string robotId) .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,6 +166,23 @@ public async Task Update(MissionRun missionRun) return missionRun; } + public async Task OngoingMission(string robotId) + { + var ongoingMissions = await ReadAll( + new MissionRunQueryStringParameters + { + Statuses = new List + { + MissionStatus.Ongoing + }, + RobotId = robotId, + OrderBy = "DesiredStartTime", + PageSize = 100 + }); + + return ongoingMissions.Any(); + } + private IQueryable GetMissionRunsWithSubModels() { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index 522cf057f..734a437dd 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.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..f5f439218 --- /dev/null +++ b/backend/api/Services/ReturnToHomeService.cs @@ -0,0 +1,81 @@ +using Api.Database.Models; +using Api.Utilities; +namespace Api.Services +{ + public interface IReturnToHomeService + { + public Task ScheduleReturnToHomeMissionRun(string robotId); + } + + public class ReturnToHomeService : IReturnToHomeService + { + private readonly ILogger _logger; + private readonly IMissionRunService _missionRunService; + private readonly IRobotService _robotService; + + public ReturnToHomeService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService) + { + _logger = logger; + _robotService = robotService; + _missionRunService = missionRunService; + } + + 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.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, + Area = null, + Status = MissionStatus.Pending, + DesiredStartTime = DateTime.UtcNow, + Tasks = new List + { + new(robot.CurrentArea.Deck.DefaultLocalizationPose.Pose, "drive_to") + }, + 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..d10b4b21f 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -172,7 +172,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 +184,10 @@ 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 != null ? r.CurrentArea.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..0e73e71c6 100644 --- a/backend/api/Utilities/Exceptions.cs +++ b/backend/api/Utilities/Exceptions.cs @@ -82,4 +82,25 @@ public class DeckExistsException(string message) : Exception(message) public class SafeZoneException(string message) : Exception(message) { } + + public class RobotNotAvailableException(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) + { + } } From b86cabbb81d2ed5030627bc9d089bc021f7a642e Mon Sep 17 00:00:00 2001 From: aestene Date: Wed, 15 Nov 2023 13:55:36 +0100 Subject: [PATCH 04/18] Add small fixes --- .../MissionSchedulingController.cs | 4 +-- backend/api/Services/LocalizationService.cs | 25 ++++++++----------- backend/api/Services/RobotService.cs | 3 ++- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index e24a62612..0ba7aa3ad 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, @@ -20,8 +21,7 @@ public class MissionSchedulingController( IMapService mapService, IStidService stidService, ILocalizationService localizationService, - ISourceService sourceService, - ICustomMissionSchedulingService customMissionSchedulingService + ISourceService sourceService ) : ControllerBase { diff --git a/backend/api/Services/LocalizationService.cs b/backend/api/Services/LocalizationService.cs index 156c2f033..87ddf51d5 100644 --- a/backend/api/Services/LocalizationService.cs +++ b/backend/api/Services/LocalizationService.cs @@ -36,7 +36,7 @@ public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionD if (missionInstallation is null || robotInstallation is null) { - string errorMessage = $"Could not find installation for installation code {missionDefinition.InstallationCode}"; + 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); } @@ -71,14 +71,14 @@ public async Task EnsureRobotIsCorrectlyLocalized(Robot robot, MissionDefinition public async Task RobotIsLocalized(string robotId) { var robot = await _robotService.ReadById(robotId); - if (robot != null) + if (robot is null) { - return robot.CurrentArea is not null; + string errorMessage = $"Robot with ID: {robotId} was not found in the database"; + _logger.LogError("{Message}", errorMessage); + throw new RobotNotFoundException(errorMessage); } - 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) @@ -91,10 +91,7 @@ public async Task EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(string ro throw new RobotNotFoundException(errorMessage); } - if (await _missionRunService.OngoingMission(robot.Id)) - { - await WaitForLocalizationMissionStatusToBeUpdated(robot); - } + if (await _missionRunService.OngoingMission(robot.Id)) { await WaitForLocalizationMissionStatusToBeUpdated(robot); } var lastExecutedMissionRun = await _missionRunService.ReadLastExecutedMissionRunByRobot(robot.Id); if (lastExecutedMissionRun is null) @@ -112,7 +109,7 @@ public async Task EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(string ro throw new LocalizationFailedException(errorMessage); } - await _robotService.SetCurrentArea(lastExecutedMissionRun.Area, robot.Id); + await _robotService.UpdateCurrentArea(robot.Id, lastExecutedMissionRun.Area); } private async Task WaitForLocalizationMissionStatusToBeUpdated(Robot robot) @@ -151,7 +148,7 @@ private async Task WaitForLocalizationMissionStatusToBeUpdated(Robot robot) while (timer.Elapsed.TotalSeconds < Timeout) { if (ongoingMissionRun is null) { continue; } - if (ongoingMissionRun.Status == MissionStatus.Successful) { break; } + if (ongoingMissionRun.Status == MissionStatus.Successful) { return; } ongoingMissionRun = await _missionRunService.ReadById(ongoingMissionRunId); } @@ -192,9 +189,7 @@ private async Task StartLocalizationMissionInArea(Robot robot, Area missionDefin }; await _missionRunService.Create(localizationMissionRun); - - robot.CurrentArea = localizationMissionRun.Area; - await _robotService.Update(robot); + await _robotService.UpdateCurrentArea(robot.Id, localizationMissionRun.Area); } private bool RobotIsOnSameDeckAsMission(Area? currentRobotArea, Area? missionArea) diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index d10b4b21f..7a44fa00a 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -184,7 +184,8 @@ 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 != null ? r.CurrentArea.Deck : 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 From 413c9d0fddb8b16d8d5322989a7e2aecb06e9ce2 Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 28 Nov 2023 10:16:10 +0100 Subject: [PATCH 05/18] Move mutex to event handler --- .../MissionSchedulingController.cs | 27 +------ .../api/EventHandlers/MissionEventHandler.cs | 21 ++++-- backend/api/Services/AreaService.cs | 10 ++- backend/api/Services/EchoService.cs | 3 +- backend/api/Services/LocalizationService.cs | 71 ++++++++++++++----- backend/api/Services/MissionRunService.cs | 19 +++-- backend/api/Services/ReturnToHomeService.cs | 3 +- 7 files changed, 92 insertions(+), 62 deletions(-) diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index 0ba7aa3ad..fedd52157 100644 --- a/backend/api/Controllers/MissionSchedulingController.cs +++ b/backend/api/Controllers/MissionSchedulingController.cs @@ -24,9 +24,7 @@ public class MissionSchedulingController( ISourceService sourceService ) : ControllerBase { - - private readonly Mutex _scheduleLocalizationMutex = new(); - + /// /// Schedule an existing mission definition /// @@ -57,19 +55,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery var missionTasks = await missionDefinitionService.GetTasksFromSource(missionDefinition.Source, missionDefinition.InstallationCode); - _scheduleLocalizationMutex.WaitOne(); - - try { await localizationService.EnsureRobotIsCorrectlyLocalized(robot, missionDefinition); } - catch (Exception e) when (e is AreaNotFoundException or DeckNotFoundException) { return NotFound(e.Message); } - catch (Exception e) when (e is RobotNotAvailableException or RobotLocalizationException) { return Conflict(e.Message); } - catch (IsarCommunicationException e) { return StatusCode(StatusCodes.Status502BadGateway, e.Message); } - - finally { _scheduleLocalizationMutex.ReleaseMutex(); } - - if (missionTasks == null) - { - return NotFound("No mission tasks were found for the requested mission"); - } + if (missionTasks == null) return NotFound("No mission tasks were found for the requested mission"); var missionRun = new MissionRun { @@ -284,15 +270,6 @@ [FromBody] CustomMissionQuery customMissionQuery catch (InstallationNotFoundException e) { return NotFound(e.Message); } catch (MissionException e) { return Conflict(e.Message); } - _scheduleLocalizationMutex.WaitOne(); - - try { await localizationService.EnsureRobotIsCorrectlyLocalized(robot, customMissionDefinition); } - catch (Exception e) when (e is AreaNotFoundException or DeckNotFoundException) { return NotFound(e.Message); } - catch (Exception e) when (e is RobotNotAvailableException or RobotLocalizationException) { return Conflict(e.Message); } - catch (IsarCommunicationException e) { return StatusCode(StatusCodes.Status502BadGateway, e.Message); } - - finally { _scheduleLocalizationMutex.ReleaseMutex(); } - 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/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index bcc103e15..e1dfe91a4 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( @@ -69,11 +71,18 @@ private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArg return; } - if (!await LocalizationService.RobotIsLocalized(missionRun.Robot.Id) && !missionRun.IsLocalizationMission()) - { - _logger.LogWarning("A mission run was created while the robot was not localized and it will be put on the queue awaiting localization"); - return; - } + string missionRunIdToStart = missionRun.Id; + + _scheduleLocalizationSemaphore.WaitOne(); + + string? localizationMissionRunId = null; + try { localizationMissionRunId = await LocalizationService.EnsureRobotIsCorrectlyLocalized(missionRun.Robot, missionRun); } + catch (Exception ex) when (ex is AreaNotFoundException or DeckNotFoundException) { return; } + catch (Exception ex) when (ex is RobotNotAvailableException or RobotLocalizationException) { return; } + catch (IsarCommunicationException) { return; } + finally { _scheduleLocalizationSemaphore.Release(); } + + if (localizationMissionRunId is not null) missionRunIdToStart = localizationMissionRunId; if (MissionScheduling.MissionRunQueueIsEmpty(await MissionService.ReadMissionRunQueue(missionRun.Robot.Id))) { @@ -82,7 +91,7 @@ private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArg } _scheduleMissionSemaphore.WaitOne(); - await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun.Id); + await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRunIdToStart); _scheduleMissionSemaphore.Release(); } diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index 5a27cc8f1..96da712ac 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -193,9 +193,13 @@ 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())); + return context.Areas + .Include(area => area.SafePositions) + .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 ReadByInstallationAndName(Installation? installation, string areaName) 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 index 87ddf51d5..bf547f671 100644 --- a/backend/api/Services/LocalizationService.cs +++ b/backend/api/Services/LocalizationService.cs @@ -5,7 +5,7 @@ namespace Api.Services { public interface ILocalizationService { - public Task EnsureRobotIsCorrectlyLocalized(Robot robot, MissionDefinition missionDefinition); + public Task EnsureRobotIsCorrectlyLocalized(Robot robot, MissionRun missionRun); public Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition); @@ -20,13 +20,15 @@ public class LocalizationService : ILocalizationService private readonly ILogger _logger; private readonly IMissionRunService _missionRunService; private readonly IRobotService _robotService; + private readonly IAreaService _areaService; - public LocalizationService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService, IInstallationService installationService) + public LocalizationService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService, IInstallationService installationService, IAreaService areaService) { _logger = logger; _robotService = robotService; _missionRunService = missionRunService; _installationService = installationService; + _areaService = areaService; } public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition) @@ -49,23 +51,29 @@ public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionD } } - public async Task EnsureRobotIsCorrectlyLocalized(Robot robot, MissionDefinition missionDefinition) + public async Task EnsureRobotIsCorrectlyLocalized(Robot robot, MissionRun missionRun) { - if (missionDefinition.Area is null) + if (missionRun.Area is null) { - string errorMessage = $"There was no area associated with mission definition {missionDefinition.Id}"; + string errorMessage = $"There was no area associated with mission run {missionRun.Id}"; _logger.LogError("{Message}", errorMessage); throw new AreaNotFoundException(errorMessage); } - if (!await RobotIsLocalized(robot.Id)) { await StartLocalizationMissionInArea(robot, missionDefinition.Area); } + string? localizationMissionRunId = null; - if (!RobotIsOnSameDeckAsMission(robot.CurrentArea, missionDefinition.Area)) + if (!await RobotIsLocalized(robot.Id)) { localizationMissionRunId = await StartLocalizationMissionInArea(robot.Id, missionRun.Area.Id); } + + if (!await RobotIsOnSameDeckAsMission(robot.Id, missionRun.Area.Id)) { - string errorMessage = $"A new mission run of mission definition {missionDefinition.Id} will not be started as the robot is not localized on the same deck as the mission"; + 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) @@ -158,9 +166,23 @@ private async Task WaitForLocalizationMissionStatusToBeUpdated(Robot robot) throw new TimeoutException(Message); } - private async Task StartLocalizationMissionInArea(Robot robot, Area missionDefinitionArea) + private async Task StartLocalizationMissionInArea(string robotId, string areaId) { - if (missionDefinitionArea.Deck?.DefaultLocalizationPose?.Pose is null) + 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); @@ -177,36 +199,47 @@ private async Task StartLocalizationMissionInArea(Robot robot, Area missionDefin { Name = "Localization mission", Robot = robot, - InstallationCode = missionDefinitionArea.Installation.InstallationCode, - Area = missionDefinitionArea, + InstallationCode = area.Installation.InstallationCode, + Area = area, Status = MissionStatus.Pending, DesiredStartTime = DateTime.UtcNow, Tasks = new List { - new(missionDefinitionArea.Deck.DefaultLocalizationPose.Pose, "localization") + new(area.Deck.DefaultLocalizationPose.Pose, "localization") }, Map = new MapMetadata() }; - - await _missionRunService.Create(localizationMissionRun); + _logger.LogWarning("Starting localization mission"); + await _missionRunService.Create(localizationMissionRun, triggerCreatedMissionRunEvent: false); await _robotService.UpdateCurrentArea(robot.Id, localizationMissionRun.Area); + return localizationMissionRun.Id; } - private bool RobotIsOnSameDeckAsMission(Area? currentRobotArea, Area? missionArea) + private async Task RobotIsOnSameDeckAsMission(string robotId, string areaId) { - if (currentRobotArea is null) + 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 (currentRobotArea?.Deck is null) + + if (robot.CurrentArea?.Deck is null) { const string ErrorMessage = "The robot area is not associated with any deck"; _logger.LogError("{Message}", ErrorMessage); @@ -219,7 +252,7 @@ private bool RobotIsOnSameDeckAsMission(Area? currentRobotArea, Area? missionAre throw new DeckNotFoundException(ErrorMessage); } - return currentRobotArea.Deck == missionArea.Deck; + return robot.CurrentArea.Deck == missionArea.Deck; } } } diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index 7a8c8b837..05625b7ac 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); @@ -57,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 @@ -68,8 +68,11 @@ public async Task Create(MissionRun missionRun) await ApplyDatabaseUpdate(missionRun.Area?.Installation); _ = 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; } @@ -189,10 +192,14 @@ private IQueryable GetMissionRunsWithSubModels() 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/ReturnToHomeService.cs b/backend/api/Services/ReturnToHomeService.cs index f5f439218..08257b277 100644 --- a/backend/api/Services/ReturnToHomeService.cs +++ b/backend/api/Services/ReturnToHomeService.cs @@ -61,7 +61,8 @@ public ReturnToHomeService(ILogger logger, IRobotService ro Name = "Return to home mission", Robot = robot, InstallationCode = robot.CurrentInstallation, - Area = null, + MissionRunPriority = MissionRunPriority.Normal, + Area = robot.CurrentArea, Status = MissionStatus.Pending, DesiredStartTime = DateTime.UtcNow, Tasks = new List From f90f009d50bcb308be13ab1a123c870f4430aa94 Mon Sep 17 00:00:00 2001 From: Afonso Date: Mon, 4 Dec 2023 13:38:13 +0100 Subject: [PATCH 06/18] Add TODO comments to unhnadled exceptions --- backend/api/EventHandlers/MissionEventHandler.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index e1dfe91a4..73778c246 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -110,9 +110,8 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) try { await LocalizationService.EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(robot.Id); } catch (Exception ex) when (ex is LocalizationFailedException or RobotNotFoundException or MissionNotFoundException or MissionException or TimeoutException) { + //TODO Handle failed localization - Cancel all the missions? _logger.LogError("Could not confirm that the robot was correctly localized and the scheduled missions for the deck will be cancelled"); - // Cancel missions - // Raise localization mission failed event? } } @@ -130,7 +129,12 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) if (!lastExecutedMissionRun.IsDriveToMission()) { try { await ReturnToHomeService.ScheduleReturnToHomeMissionRun(robot.Id); } - catch (Exception ex) when (ex is RobotNotFoundException or AreaNotFoundException or DeckNotFoundException or PoseNotFoundException) { return; } + catch (Exception ex) when (ex is RobotNotFoundException or AreaNotFoundException or DeckNotFoundException or PoseNotFoundException) + { + //TODO Handle failed return to home - Try again for a number of times? + _logger.LogError("Could not schedule a mission to send the robot back to its safe position"); + return; + } } else { await RobotService.UpdateCurrentArea(robot.Id, null); } return; From 8973db5c4d027311ce4b9bb170f31739180123d4 Mon Sep 17 00:00:00 2001 From: Afonso Date: Mon, 4 Dec 2023 13:49:57 +0100 Subject: [PATCH 07/18] Refactor to remove dotnet format warnings --- .../Mocks/CustomMissionServiceMock.cs | 2 + backend/api/Database/Models/MissionRun.cs | 2 +- backend/api/Services/CustomMissionService.cs | 1 - backend/api/Services/LocalizationService.cs | 109 ++++++++---------- backend/api/Services/MissionRunService.cs | 5 +- backend/api/Services/ReturnToHomeService.cs | 29 ++--- backend/api/Utilities/Exceptions.cs | 2 +- 7 files changed, 63 insertions(+), 87 deletions(-) diff --git a/backend/api.test/Mocks/CustomMissionServiceMock.cs b/backend/api.test/Mocks/CustomMissionServiceMock.cs index 89c1b3d72..b95831901 100644 --- a/backend/api.test/Mocks/CustomMissionServiceMock.cs +++ b/backend/api.test/Mocks/CustomMissionServiceMock.cs @@ -44,10 +44,12 @@ public string CalculateHashFromTasks(IList tasks) 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/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index ce978311b..edad78b41 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -139,7 +139,7 @@ public void CalculateEstimatedDuration() task => task.Inspections.Sum(inspection => inspection.VideoDuration ?? 0) ); EstimatedDuration = (uint)( - Robot.Model.AverageDurationPerTag * Tasks.Count + totalInspectionDuration + (Robot.Model.AverageDurationPerTag * Tasks.Count) + totalInspectionDuration ); } else diff --git a/backend/api/Services/CustomMissionService.cs b/backend/api/Services/CustomMissionService.cs index 51060d680..d19ca83b3 100644 --- a/backend/api/Services/CustomMissionService.cs +++ b/backend/api/Services/CustomMissionService.cs @@ -1,7 +1,6 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using Api.Controllers.Models; using Api.Database.Models; using Api.Options; using Microsoft.Extensions.Options; diff --git a/backend/api/Services/LocalizationService.cs b/backend/api/Services/LocalizationService.cs index bf547f671..f8898f1bf 100644 --- a/backend/api/Services/LocalizationService.cs +++ b/backend/api/Services/LocalizationService.cs @@ -14,39 +14,25 @@ public interface ILocalizationService public Task RobotIsLocalized(string robotId); } - public class LocalizationService : ILocalizationService + public class LocalizationService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService, IInstallationService installationService, IAreaService areaService) : ILocalizationService { - private readonly IInstallationService _installationService; - private readonly ILogger _logger; - private readonly IMissionRunService _missionRunService; - private readonly IRobotService _robotService; - private readonly IAreaService _areaService; - - public LocalizationService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService, IInstallationService installationService, IAreaService areaService) - { - _logger = logger; - _robotService = robotService; - _missionRunService = missionRunService; - _installationService = installationService; - _areaService = areaService; - } public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition) { - var missionInstallation = await _installationService.ReadByName(missionDefinition.InstallationCode); - var robotInstallation = await _installationService.ReadByName(robot.CurrentInstallation); + var missionInstallation = await installationService.ReadByName(missionDefinition.InstallationCode); + var robotInstallation = await installationService.ReadByName(robot.CurrentInstallation); if (missionInstallation is null || robotInstallation is null) { string errorMessage = $"Could not find installation for installation code {missionDefinition.InstallationCode} or the robot has no current installation"; - _logger.LogError("{Message}", errorMessage); + logger.LogError("{Message}", errorMessage); throw new InstallationNotFoundException(errorMessage); } if (robotInstallation != missionInstallation) { string errorMessage = $"The robot {robot.Name} is on installation {robotInstallation.Name} which is not the same as the mission installation {missionInstallation.Name}"; - _logger.LogError("{Message}", errorMessage); + logger.LogError("{Message}", errorMessage); throw new MissionException(errorMessage); } } @@ -56,7 +42,7 @@ public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionD if (missionRun.Area is null) { string errorMessage = $"There was no area associated with mission run {missionRun.Id}"; - _logger.LogError("{Message}", errorMessage); + logger.LogError("{Message}", errorMessage); throw new AreaNotFoundException(errorMessage); } @@ -67,22 +53,22 @@ public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionD 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); + logger.LogError("{Message}", errorMessage); throw new RobotLocalizationException(errorMessage); } - _logger.LogWarning("{Message}", $"Localization mission run ID is {localizationMissionRunId}"); - + logger.LogWarning("{Message}", $"Localization mission run ID is {localizationMissionRunId}"); + return localizationMissionRunId; } public async Task RobotIsLocalized(string robotId) { - var robot = await _robotService.ReadById(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); + logger.LogError("{Message}", errorMessage); throw new RobotNotFoundException(errorMessage); } @@ -91,21 +77,21 @@ public async Task RobotIsLocalized(string robotId) public async Task EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(string robotId) { - var robot = await _robotService.ReadById(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); + logger.LogError("{Message}", errorMessage); throw new RobotNotFoundException(errorMessage); } - if (await _missionRunService.OngoingMission(robot.Id)) { await WaitForLocalizationMissionStatusToBeUpdated(robot); } + if (await missionRunService.OngoingMission(robot.Id)) { await WaitForLocalizationMissionStatusToBeUpdated(robot); } - var lastExecutedMissionRun = await _missionRunService.ReadLastExecutedMissionRunByRobot(robot.Id); + 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); + logger.LogError("{Message}", errorMessage); throw new MissionNotFoundException(errorMessage); } @@ -113,11 +99,11 @@ public async Task EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(string ro { 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); + logger.LogError("{Message}", errorMessage); throw new LocalizationFailedException(errorMessage); } - await _robotService.UpdateCurrentArea(robot.Id, lastExecutedMissionRun.Area); + await robotService.UpdateCurrentArea(robot.Id, lastExecutedMissionRun.Area); } private async Task WaitForLocalizationMissionStatusToBeUpdated(Robot robot) @@ -125,32 +111,32 @@ 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); + logger.LogError("{Message}", errorMessage); throw new MissionNotFoundException(errorMessage); } string ongoingMissionRunId = robot.CurrentMissionId; - var ongoingMissionRun = await _missionRunService.ReadById(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); + 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); + logger.LogError("{Message}", errorMessage); throw new MissionException(errorMessage); } - _logger.LogWarning( + 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); + ongoingMissionRun = await missionRunService.ReadById(ongoingMissionRunId); timer.Start(); while (timer.Elapsed.TotalSeconds < Timeout) @@ -158,40 +144,42 @@ private async Task WaitForLocalizationMissionStatusToBeUpdated(Robot robot) if (ongoingMissionRun is null) { continue; } if (ongoingMissionRun.Status == MissionStatus.Successful) { return; } - ongoingMissionRun = await _missionRunService.ReadById(ongoingMissionRunId); + 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); + 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){ + var robot = await robotService.ReadById(robotId); + if (robot is null) + { string errorMessage = $"The robot with ID {robotId} was not found"; - _logger.LogError("{Message}", errorMessage); + logger.LogError("{Message}", errorMessage); throw new RobotNotFoundException(errorMessage); } - var area = await _areaService.ReadById(areaId); - if (area is null){ + var area = await areaService.ReadById(areaId); + if (area is null) + { string errorMessage = $"The area with ID {areaId} was not found"; - _logger.LogError("{Message}", errorMessage); + 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); + 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.ToString()}"; - _logger.LogWarning("{Message}", errorMessage); + string errorMessage = $"Robot '{robot.Id}' is not available as the status is {robot.Status}"; + logger.LogWarning("{Message}", errorMessage); throw new RobotNotAvailableException(errorMessage); } @@ -209,46 +197,47 @@ private async Task StartLocalizationMissionInArea(string robotId, string }, Map = new MapMetadata() }; - _logger.LogWarning("Starting localization mission"); - await _missionRunService.Create(localizationMissionRun, triggerCreatedMissionRunEvent: false); - await _robotService.UpdateCurrentArea(robot.Id, localizationMissionRun.Area); + 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){ + var robot = await robotService.ReadById(robotId); + if (robot is null) + { string errorMessage = $"The robot with ID {robotId} was not found"; - _logger.LogError("{Message}", errorMessage); + 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); + logger.LogError("{Message}", ErrorMessage); throw new AreaNotFoundException(ErrorMessage); } - var missionArea = await _areaService.ReadById(areaId); + 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); + 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); + 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); + logger.LogError("{Message}", ErrorMessage); throw new DeckNotFoundException(ErrorMessage); } diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index 05625b7ac..be6aace82 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -174,10 +174,7 @@ public async Task OngoingMission(string robotId) var ongoingMissions = await ReadAll( new MissionRunQueryStringParameters { - Statuses = new List - { - MissionStatus.Ongoing - }, + Statuses = [MissionStatus.Ongoing], RobotId = robotId, OrderBy = "DesiredStartTime", PageSize = 100 diff --git a/backend/api/Services/ReturnToHomeService.cs b/backend/api/Services/ReturnToHomeService.cs index 08257b277..2698151b6 100644 --- a/backend/api/Services/ReturnToHomeService.cs +++ b/backend/api/Services/ReturnToHomeService.cs @@ -7,52 +7,41 @@ public interface IReturnToHomeService public Task ScheduleReturnToHomeMissionRun(string robotId); } - public class ReturnToHomeService : IReturnToHomeService + public class ReturnToHomeService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService) : IReturnToHomeService { - private readonly ILogger _logger; - private readonly IMissionRunService _missionRunService; - private readonly IRobotService _robotService; - - public ReturnToHomeService(ILogger logger, IRobotService robotService, IMissionRunService missionRunService) - { - _logger = logger; - _robotService = robotService; - _missionRunService = missionRunService; - } - public async Task ScheduleReturnToHomeMissionRun(string robotId) { - var robot = await _robotService.ReadById(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); + logger.LogError("{Message}", errorMessage); throw new RobotNotFoundException(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); + 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); + logger.LogError("{Message}", errorMessage); throw new DeckNotFoundException(errorMessage); } if (robot.CurrentArea.Deck.DefaultLocalizationPose is null) { - _logger.LogError( + 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); + logger.LogError("{Message}", errorMessage); throw new PoseNotFoundException(errorMessage); } @@ -72,8 +61,8 @@ public ReturnToHomeService(ILogger logger, IRobotService ro Map = new MapMetadata() }; - var missionRun = await _missionRunService.Create(returnToHomeMissionRun); - _logger.LogInformation( + 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/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs index 0e73e71c6..fef5432b3 100644 --- a/backend/api/Utilities/Exceptions.cs +++ b/backend/api/Utilities/Exceptions.cs @@ -86,7 +86,7 @@ public class SafeZoneException(string message) : Exception(message) public class RobotNotAvailableException(string message) : Exception(message) { } - + public class PoseNotFoundException(string message) : Exception(message) { From d9d76a3b876a60fef57dac69d3a44f065fa780ca Mon Sep 17 00:00:00 2001 From: Afonso Date: Wed, 6 Dec 2023 09:48:03 +0100 Subject: [PATCH 08/18] Refactor exceptions --- .../MissionSchedulingController.cs | 5 ++- .../api/EventHandlers/MissionEventHandler.cs | 31 +++++++++++++------ backend/api/Services/LocalizationService.cs | 4 +-- backend/api/Utilities/Exceptions.cs | 6 ++++ 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index fedd52157..d9be88ee9 100644 --- a/backend/api/Controllers/MissionSchedulingController.cs +++ b/backend/api/Controllers/MissionSchedulingController.cs @@ -51,10 +51,9 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery try { await localizationService.EnsureRobotIsOnSameInstallationAsMission(robot, missionDefinition); } catch (InstallationNotFoundException e) { return NotFound(e.Message); } - catch (MissionException e) { return Conflict(e.Message); } + catch (RobotNotInSameInstallationAsMissionException e) { return Conflict(e.Message); } 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 @@ -268,7 +267,7 @@ [FromBody] CustomMissionQuery customMissionQuery try { await localizationService.EnsureRobotIsOnSameInstallationAsMission(robot, customMissionDefinition); } catch (InstallationNotFoundException e) { return NotFound(e.Message); } - catch (MissionException e) { return Conflict(e.Message); } + catch (RobotNotInSameInstallationAsMissionException e) { return Conflict(e.Message); } MissionRun? newMissionRun; try { newMissionRun = await customMissionSchedulingService.QueueCustomMissionRun(customMissionQuery, customMissionDefinition.Id, robot.Id, missionTasks); } diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 73778c246..345cd22d6 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -64,24 +64,32 @@ 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; } - string missionRunIdToStart = missionRun.Id; _scheduleLocalizationSemaphore.WaitOne(); string? localizationMissionRunId = null; try { localizationMissionRunId = await LocalizationService.EnsureRobotIsCorrectlyLocalized(missionRun.Robot, missionRun); } - catch (Exception ex) when (ex is AreaNotFoundException or DeckNotFoundException) { return; } - catch (Exception ex) when (ex is RobotNotAvailableException or RobotLocalizationException) { return; } - catch (IsarCommunicationException) { return; } + catch (Exception ex) when ( + ex is AreaNotFoundException + or DeckNotFoundException + or RobotNotAvailableException + or RobotLocalizationException + or RobotNotFoundException + or IsarCommunicationException + ) + { + //TODO Cancel the mission? + return; + } finally { _scheduleLocalizationSemaphore.Release(); } + string missionRunIdToStart = missionRun.Id; if (localizationMissionRunId is not null) missionRunIdToStart = localizationMissionRunId; if (MissionScheduling.MissionRunQueueIsEmpty(await MissionService.ReadMissionRunQueue(missionRun.Robot.Id))) @@ -91,7 +99,8 @@ private async void OnMissionRunCreated(object? sender, MissionRunCreatedEventArg } _scheduleMissionSemaphore.WaitOne(); - await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRunIdToStart); + try { await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRunIdToStart); } + catch (MissionRunNotFoundException) { return; } _scheduleMissionSemaphore.Release(); } @@ -105,10 +114,11 @@ 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 MissionException or TimeoutException) + catch (Exception ex) when (ex is LocalizationFailedException or RobotNotFoundException or MissionNotFoundException or OngoingMissionNotLocalizationException or TimeoutException) { //TODO Handle failed localization - Cancel all the missions? _logger.LogError("Could not confirm that the robot was correctly localized and the scheduled missions for the deck will be cancelled"); @@ -131,8 +141,8 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) try { await ReturnToHomeService.ScheduleReturnToHomeMissionRun(robot.Id); } catch (Exception ex) when (ex is RobotNotFoundException or AreaNotFoundException or DeckNotFoundException or PoseNotFoundException) { - //TODO Handle failed return to home - Try again for a number of times? - _logger.LogError("Could not schedule a mission to send the robot back to its safe position"); + //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; } } @@ -151,7 +161,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/Services/LocalizationService.cs b/backend/api/Services/LocalizationService.cs index f8898f1bf..55a62338c 100644 --- a/backend/api/Services/LocalizationService.cs +++ b/backend/api/Services/LocalizationService.cs @@ -33,7 +33,7 @@ public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionD { string errorMessage = $"The robot {robot.Name} is on installation {robotInstallation.Name} which is not the same as the mission installation {missionInstallation.Name}"; logger.LogError("{Message}", errorMessage); - throw new MissionException(errorMessage); + throw new RobotNotInSameInstallationAsMissionException(errorMessage); } } @@ -128,7 +128,7 @@ private async Task WaitForLocalizationMissionStatusToBeUpdated(Robot robot) { string errorMessage = $"The currently executing mission for robot {robot.CurrentMissionId} is not a localization mission"; logger.LogError("{Message}", errorMessage); - throw new MissionException(errorMessage); + throw new OngoingMissionNotLocalizationException(errorMessage); } logger.LogWarning( diff --git a/backend/api/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs index fef5432b3..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) { @@ -87,6 +90,9 @@ public class RobotNotAvailableException(string message) : Exception(message) { } + public class RobotNotInSameInstallationAsMissionException(string message) : Exception(message) + { + } public class PoseNotFoundException(string message) : Exception(message) { From b878847de80506d782de6a4148eefb3d2acf1212 Mon Sep 17 00:00:00 2001 From: Afonso Date: Wed, 6 Dec 2023 09:48:14 +0100 Subject: [PATCH 09/18] Test localization functionality and fix failing tests --- backend/api.test/Client/AreaTests.cs | 9 +- backend/api.test/Client/MissionTests.cs | 248 ++++++++++++++++-- .../api.test/Database/DatabaseUtilities.cs | 7 +- .../EventHandlers/TestMissionEventHandler.cs | 22 +- backend/api.test/Services/RobotService.cs | 16 +- .../Controllers/Models/CreateRobotQuery.cs | 2 + backend/api/Services/AreaService.cs | 1 + backend/api/Services/LocalizationService.cs | 7 +- backend/api/Services/MissionRunService.cs | 4 +- backend/api/Services/ReturnToHomeService.cs | 9 +- backend/api/Services/RobotService.cs | 16 +- 11 files changed, 285 insertions(+), 56 deletions(-) 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..9e903a144 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 = "RobotGetNextRun", + SerialNumber = "GetNextRun", + 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 - Create custom mission definition + 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/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/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/Services/AreaService.cs b/backend/api/Services/AreaService.cs index 96da712ac..818c90c8d 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -195,6 +195,7 @@ private IQueryable GetAreas() var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); 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) diff --git a/backend/api/Services/LocalizationService.cs b/backend/api/Services/LocalizationService.cs index 55a62338c..16cf6a10e 100644 --- a/backend/api/Services/LocalizationService.cs +++ b/backend/api/Services/LocalizationService.cs @@ -20,18 +20,17 @@ public class LocalizationService(ILogger logger, IRobotServ public async Task EnsureRobotIsOnSameInstallationAsMission(Robot robot, MissionDefinition missionDefinition) { var missionInstallation = await installationService.ReadByName(missionDefinition.InstallationCode); - var robotInstallation = await installationService.ReadByName(robot.CurrentInstallation); - if (missionInstallation is null || robotInstallation is null) + 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 (robotInstallation != missionInstallation) + if (robot.CurrentInstallation != missionInstallation) { - string errorMessage = $"The robot {robot.Name} is on installation {robotInstallation.Name} which is not the same as the mission installation {missionInstallation.Name}"; + string errorMessage = $"The robot {robot.Name} is on installation {robot.CurrentInstallation.Name} which is not the same as the mission installation {missionInstallation.Name}"; logger.LogError("{Message}", errorMessage); throw new RobotNotInSameInstallationAsMissionException(errorMessage); } diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index be6aace82..3f3754f9c 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -66,7 +66,7 @@ public async Task Create(MissionRun missionRun, bool triggerCreatedM 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); if (triggerCreatedMissionRunEvent) { @@ -128,6 +128,8 @@ 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) diff --git a/backend/api/Services/ReturnToHomeService.cs b/backend/api/Services/ReturnToHomeService.cs index 2698151b6..f7ac9078d 100644 --- a/backend/api/Services/ReturnToHomeService.cs +++ b/backend/api/Services/ReturnToHomeService.cs @@ -19,6 +19,13 @@ public class ReturnToHomeService(ILogger logger, IRobotServ 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"; @@ -49,7 +56,7 @@ public class ReturnToHomeService(ILogger logger, IRobotServ { Name = "Return to home mission", Robot = robot, - InstallationCode = robot.CurrentInstallation, + InstallationCode = robot.CurrentInstallation.InstallationCode, MissionRunPriority = MissionRunPriority.Normal, Area = robot.CurrentArea, Status = MissionStatus.Pending, diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index 7a44fa00a..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 }; From 0a5a0ace5c379f6080b7c2837533c757674f37c4 Mon Sep 17 00:00:00 2001 From: Afonso Luz Date: Sat, 16 Dec 2023 17:55:30 +0100 Subject: [PATCH 10/18] Cancel missions if localization fails --- backend/api.test/Client/MissionTests.cs | 6 ++-- .../api/EventHandlers/MissionEventHandler.cs | 13 ++++++-- .../api/Services/MissionSchedulingService.cs | 31 +++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/backend/api.test/Client/MissionTests.cs b/backend/api.test/Client/MissionTests.cs index 9e903a144..5737653b4 100644 --- a/backend/api.test/Client/MissionTests.cs +++ b/backend/api.test/Client/MissionTests.cs @@ -740,8 +740,8 @@ public async Task MissionFailsIfRobotIsNotInSameDeckAsMission() var robotQuery = new CreateRobotQuery { IsarId = Guid.NewGuid().ToString(), - Name = "RobotGetNextRun", - SerialNumber = "GetNextRun", + Name = "RobotMissionFailsIfRobotIsNotInSameDeckAsMission", + SerialNumber = "GetMissionFailsIfRobotIsNotInSameDeckAsMission", RobotType = RobotType.Robot, Status = RobotStatus.Available, Enabled = true, @@ -756,7 +756,7 @@ public async Task MissionFailsIfRobotIsNotInSameDeckAsMission() var robot = await PostToDb(robotUrl, robotQuery); string robotId = robot.Id; - // Arrange - Create custom mission definition + // Arrange - Mission Run Query var query = new CustomMissionQuery { RobotId = robotId, diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 345cd22d6..832d1ebba 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -84,7 +84,9 @@ or RobotNotFoundException or IsarCommunicationException ) { - //TODO Cancel the mission? + _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(); } @@ -120,8 +122,13 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) try { await LocalizationService.EnsureRobotWasCorrectlyLocalizedInPreviousMissionRun(robot.Id); } catch (Exception ex) when (ex is LocalizationFailedException or RobotNotFoundException or MissionNotFoundException or OngoingMissionNotLocalizationException or TimeoutException) { - //TODO Handle failed localization - Cancel all the missions? - _logger.LogError("Could not confirm that the robot was correctly localized and the scheduled missions for the deck will be cancelled"); + _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; + } } } 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; } From 0fb47b005ae638eefc062f437b6da31d654c07b4 Mon Sep 17 00:00:00 2001 From: Afonso Luz Date: Sun, 17 Dec 2023 21:30:43 +0100 Subject: [PATCH 11/18] Use Enum for MissionTask Type --- backend/api/Database/Context/InitDb.cs | 2 +- backend/api/Database/Models/MissionRun.cs | 4 +-- backend/api/Database/Models/MissionTask.cs | 29 +++++++++++++++---- backend/api/Services/LocalizationService.cs | 2 +- .../Services/Models/IsarMissionDefinition.cs | 2 +- backend/api/Services/ReturnToHomeService.cs | 2 +- 6 files changed, 29 insertions(+), 12 deletions(-) diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index be32ee4a8..144776666 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -41,7 +41,7 @@ public static class InitDb EchoTagLink = new Uri("https://www.I-am-echo-stid-tag-url.com"), InspectionTarget = new Position(), RobotPose = new Pose(), - Type = "inspection" + Type = MissionTaskType.Inspection }; private static List GetAccessRoles() diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index edad78b41..181bdd0ec 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -195,7 +195,7 @@ public bool IsLocalizationMission() { return false; } - return Tasks[0].Type == "localization"; + return Tasks[0].Type == MissionTaskType.Localization; } public bool IsDriveToMission() @@ -204,7 +204,7 @@ public bool IsDriveToMission() { return false; } - return Tasks[0].Type == "drive_to"; + return Tasks[0].Type == MissionTaskType.DriveTo; } } diff --git a/backend/api/Database/Models/MissionTask.cs b/backend/api/Database/Models/MissionTask.cs index 1f80da322..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,7 +28,7 @@ public MissionTask(EchoTag echoTag, Position tagPosition) EchoPoseId = echoTag.PoseId; TaskOrder = echoTag.PlanOrder; Status = TaskStatus.NotStarted; - Type = "inspection"; + Type = MissionTaskType.Inspection; } // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized @@ -41,14 +43,14 @@ public MissionTask(CustomTaskQuery taskQuery) RobotPose = taskQuery.RobotPose; TaskOrder = taskQuery.TaskOrder; Status = TaskStatus.NotStarted; - Type = "inspection"; + Type = MissionTaskType.Inspection; } - public MissionTask(Pose robotPose, string type) + public MissionTask(Pose robotPose, MissionTaskType type) { switch (type) { - case "localization": + case MissionTaskType.Localization: Type = type; Description = "Localization"; RobotPose = robotPose; @@ -57,7 +59,7 @@ public MissionTask(Pose robotPose, string type) InspectionTarget = new Position(); Inspections = new List(); break; - case "drive_to": + case MissionTaskType.DriveTo: Type = type; Description = "Return to home"; RobotPose = robotPose; @@ -66,6 +68,8 @@ public MissionTask(Pose robotPose, string type) InspectionTarget = new Position(); Inspections = new List(); break; + default: + throw new MissionTaskNotFoundException("MissionTaskType should be Localization or DriveTo"); } } @@ -95,7 +99,7 @@ public MissionTask(MissionTask copy) [Required] public int TaskOrder { get; set; } - public string Type { get; set; } + public MissionTaskType Type { get; set; } [MaxLength(200)] public string? TagId { get; set; } @@ -175,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 @@ -187,4 +197,11 @@ public enum TaskStatus Cancelled, Paused } + + public enum MissionTaskType + { + Inspection, + Localization, + DriveTo + } } diff --git a/backend/api/Services/LocalizationService.cs b/backend/api/Services/LocalizationService.cs index 16cf6a10e..8e6f6f794 100644 --- a/backend/api/Services/LocalizationService.cs +++ b/backend/api/Services/LocalizationService.cs @@ -192,7 +192,7 @@ private async Task StartLocalizationMissionInArea(string robotId, string DesiredStartTime = DateTime.UtcNow, Tasks = new List { - new(area.Deck.DefaultLocalizationPose.Pose, "localization") + new(area.Deck.DefaultLocalizationPose.Pose, MissionTaskType.Localization) }, Map = new MapMetadata() }; diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index 734a437dd..34eac19d6 100644 --- a/backend/api/Services/Models/IsarMissionDefinition.cs +++ b/backend/api/Services/Models/IsarMissionDefinition.cs @@ -52,7 +52,7 @@ public struct IsarTaskDefinition public IsarTaskDefinition(MissionTask missionTask, MissionRun missionRun) { Id = missionTask.IsarTaskId; - Type = missionTask.Type; + 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 index f7ac9078d..4e72432b0 100644 --- a/backend/api/Services/ReturnToHomeService.cs +++ b/backend/api/Services/ReturnToHomeService.cs @@ -63,7 +63,7 @@ public class ReturnToHomeService(ILogger logger, IRobotServ DesiredStartTime = DateTime.UtcNow, Tasks = new List { - new(robot.CurrentArea.Deck.DefaultLocalizationPose.Pose, "drive_to") + new(robot.CurrentArea.Deck.DefaultLocalizationPose.Pose, MissionTaskType.DriveTo) }, Map = new MapMetadata() }; From 12d6a8f03ae6a1b30d808f675528b8cdf81952f4 Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 19 Dec 2023 12:41:42 +0100 Subject: [PATCH 12/18] Set default safe positions for areas --- backend/api/Database/Context/InitDb.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index 144776666..85bfe31cf 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -169,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 @@ -181,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 @@ -193,7 +193,7 @@ private static List GetAreas() Name = "testArea", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List() + SafePositions = new List(new []{new SafePosition()}) }; var area4 = new Area @@ -205,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 @@ -217,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 @@ -229,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 From 6337c4fe6cdf90c9b7e882f5ff8138ea66a2474d Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 19 Dec 2023 13:00:26 +0100 Subject: [PATCH 13/18] Remove redundant function --- .../api/Services/MissionDefinitionService.cs | 53 +------------------ 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index b2b3cace6..cf6d2fc0f 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -23,9 +23,7 @@ public interface IMissionDefinitionService public Task?> GetTasksFromSource(Source source, string installationCodes); public Task> ReadBySourceId(string sourceId); - - public Task FindExistingOrCreateCustomMissionDefinition(CustomMissionQuery customMissionQuery, List missionTasks); - + public Task Update(MissionDefinition missionDefinition); public Task Delete(string id); @@ -171,55 +169,6 @@ private async Task ApplyDatabaseUpdate(Installation? installation) } - public async Task FindExistingOrCreateCustomMissionDefinition(CustomMissionQuery customMissionQuery, List missionTasks) - { - Area? area = null; - if (customMissionQuery.AreaName != null) { area = await areaService.ReadByInstallationAndName(customMissionQuery.InstallationCode, customMissionQuery.AreaName); } - - var source = await sourceService.CheckForExistingCustomSource(missionTasks); - - MissionDefinition? existingMissionDefinition = null; - if (source == null) - { - try - { - string sourceUrl = await customMissionService.UploadSource(missionTasks); - source = new Source - { - SourceId = sourceUrl, - Type = MissionSourceType.Custom - }; - } - catch (Exception e) - { - { - string errorMessage = $"Unable to upload source for mission {customMissionQuery.Name}"; - logger.LogError(e, "{Message}", errorMessage); - throw new SourceException(errorMessage); - } - } - } - else - { - var missionDefinitions = await ReadBySourceId(source.SourceId); - if (missionDefinitions.Count > 0) { existingMissionDefinition = missionDefinitions.First(); } - } - - var customMissionDefinition = existingMissionDefinition ?? new MissionDefinition - { - Id = Guid.NewGuid().ToString(), - Source = source, - Name = customMissionQuery.Name, - InspectionFrequency = customMissionQuery.InspectionFrequency, - InstallationCode = customMissionQuery.InstallationCode, - Area = area - }; - - if (existingMissionDefinition == null) { await Create(customMissionDefinition); } - - return customMissionDefinition; - } - private IQueryable GetMissionDefinitionsWithSubModels() { var accessibleInstallationCodes = accessRoleService.GetAllowedInstallationCodes(); From 1f394f676be0b9189e484e433113f5e54ef96b85 Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 19 Dec 2023 13:17:41 +0100 Subject: [PATCH 14/18] Simplify mission type checks --- backend/api/Database/Models/MissionRun.cs | 18 ++---------------- .../api/Services/MissionDefinitionService.cs | 2 +- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 181bdd0ec..e39fb8d0a 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -189,23 +189,9 @@ var inspection in task.Inspections.Where(inspection => !inspection.IsCompleted) } } - public bool IsLocalizationMission() - { - if (Tasks.Count != 1) - { - return false; - } - return Tasks[0].Type == MissionTaskType.Localization; - } + public bool IsLocalizationMission() { return Tasks is [{ Type: MissionTaskType.Localization }]; } - public bool IsDriveToMission() - { - if (Tasks.Count != 1) - { - return false; - } - return Tasks[0].Type == MissionTaskType.DriveTo; - } + public bool IsDriveToMission() { return Tasks is [{ Type: MissionTaskType.DriveTo }]; } } public enum MissionStatus diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index cf6d2fc0f..fab6a68a5 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -23,7 +23,7 @@ public interface IMissionDefinitionService public Task?> GetTasksFromSource(Source source, string installationCodes); public Task> ReadBySourceId(string sourceId); - + public Task Update(MissionDefinition missionDefinition); public Task Delete(string id); From 48b73918bd3032ecb7119c34aec19b7fcf6ed3dd Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 19 Dec 2023 13:23:26 +0100 Subject: [PATCH 15/18] Ensure semaphore is released in case of exception --- backend/api/EventHandlers/MissionEventHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 832d1ebba..49d97761d 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -102,8 +102,8 @@ or IsarCommunicationException _scheduleMissionSemaphore.WaitOne(); try { await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRunIdToStart); } - catch (MissionRunNotFoundException) { return; } - _scheduleMissionSemaphore.Release(); + catch (MissionRunNotFoundException) { } + finally{ _scheduleMissionSemaphore.Release(); } } private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) From 5ab2e779ebcf2e97619758078e135705d9430b35 Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 19 Dec 2023 13:40:03 +0100 Subject: [PATCH 16/18] Use GetAreas function in AreaService --- backend/api/Services/AreaService.cs | 34 +++++------------------------ 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index 818c90c8d..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,13 +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.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) - .ThenInclude(d => d != null ? d.DefaultLocalizationPose : null) - .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) @@ -84,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.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) @@ -203,16 +189,6 @@ private IQueryable GetAreas() .Where((area) => accessibleInstallationCodes.Result.Contains(area.Installation.InstallationCode.ToUpper())); } - public async Task ReadByInstallationAndName(Installation? installation, string areaName) - { - if (installation == null) { return null; } - - return await context.Areas.Where(a => - a.Name.ToLower().Equals(areaName.ToLower()) && a.Installation.Id.Equals(installation.Id) - ).Include(a => a.SafePositions).Include(a => a.Installation) - .Include(a => a.Plant).Include(a => a.Deck).FirstOrDefaultAsync(); - } - public async Task ReadByInstallationAndPlantAndDeckAndName(Installation installation, Plant plant, Deck deck, string areaName) { return await GetAreas().Where(a => From 925a0c6baefd6b053e9f961c862b745fce2c075f Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 19 Dec 2023 13:47:50 +0100 Subject: [PATCH 17/18] Fix formatting issues --- backend/api/Database/Context/InitDb.cs | 12 ++++++------ backend/api/EventHandlers/MissionEventHandler.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index 85bfe31cf..2cf8a23ff 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -169,7 +169,7 @@ private static List GetAreas() Name = "AP320", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List(new []{new SafePosition()}) + SafePositions = new List(new[] { new SafePosition() }) }; var area2 = new Area @@ -181,7 +181,7 @@ private static List GetAreas() Name = "AP330", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List(new []{new SafePosition()}) + SafePositions = new List(new[] { new SafePosition() }) }; var area3 = new Area @@ -193,7 +193,7 @@ private static List GetAreas() Name = "testArea", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List(new []{new SafePosition()}) + SafePositions = new List(new[] { new SafePosition() }) }; var area4 = new Area @@ -205,7 +205,7 @@ private static List GetAreas() Name = "testArea2", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List(new []{new SafePosition()}) + SafePositions = new List(new[] { new SafePosition() }) }; var area5 = new Area @@ -217,7 +217,7 @@ private static List GetAreas() Name = "testArea3", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List(new []{new SafePosition()}) + SafePositions = new List(new[] { new SafePosition() }) }; var area6 = new Area @@ -229,7 +229,7 @@ private static List GetAreas() Name = "testArea4", MapMetadata = new MapMetadata(), DefaultLocalizationPose = new DefaultLocalizationPose(), - SafePositions = new List(new []{new SafePosition()}) + SafePositions = new List(new[] { new SafePosition() }) }; var areaHuldraHB = new Area diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 49d97761d..60ea5f2a6 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -103,7 +103,7 @@ or IsarCommunicationException _scheduleMissionSemaphore.WaitOne(); try { await MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRunIdToStart); } catch (MissionRunNotFoundException) { } - finally{ _scheduleMissionSemaphore.Release(); } + finally { _scheduleMissionSemaphore.Release(); } } private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) From 7d4e99988f1f55c4443f60d969955c6618f431f8 Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 19 Dec 2023 14:01:59 +0100 Subject: [PATCH 18/18] Remove redundant services --- backend/api/Services/MissionDefinitionService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index fab6a68a5..52bb93c64 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -40,12 +40,10 @@ public interface IMissionDefinitionService Justification = "Entity framework does not support translating culture info to SQL calls" )] public class MissionDefinitionService(FlotillaDbContext context, - IAreaService areaService, IEchoService echoService, IStidService stidService, ICustomMissionService customMissionService, ISignalRService signalRService, - ISourceService sourceService, IAccessRoleService accessRoleService, ILogger logger) : IMissionDefinitionService {