From 8e27825f9178e129e27e2fe7913fffce3351c9ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Fri, 11 Oct 2024 10:12:17 +0200 Subject: [PATCH 1/6] Freeze mission queue before changing robot state --- backend/api/EventHandlers/MissionEventHandler.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 421688d8d..b02942a76 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -169,6 +169,9 @@ private async void OnSendRobotToSafezoneTriggered(object? sender, RobotEmergency return; } + try { await MissionScheduling.FreezeMissionRunQueueForRobot(e.RobotId); } + catch (RobotNotFoundException) { return; } + if (robot.FlotillaStatus == e.RobotFlotillaStatus) { _logger.LogInformation("Did not send robot to safezone since robot {RobotId} was already in the correct state", e.RobotId); @@ -182,10 +185,6 @@ private async void OnSendRobotToSafezoneTriggered(object? sender, RobotEmergency return; } - try { await MissionScheduling.FreezeMissionRunQueueForRobot(e.RobotId); } - catch (RobotNotFoundException) { return; } - - Area? area; try { From abc901e8f47f9ac1b1b6cc99440179539dc54b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Fri, 11 Oct 2024 13:29:57 +0200 Subject: [PATCH 2/6] Use localization pose when no safezone available --- .../api/Services/MissionSchedulingService.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index 0094beff6..09d0ab66f 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -288,13 +288,25 @@ public async Task ScheduleMissionToDriveToSafePosition(string robotId, string ar logger.LogError("Could not find area with ID {AreaId}", areaId); return; } + var robot = await robotService.ReadById(robotId, readOnly: true); if (robot == null) { logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId); return; } + var closestSafePosition = ClosestSafePosition(robot.Pose, area.SafePositions); + if (closestSafePosition == null) + { + logger.LogWarning("Robot with ID: {RobotId} did not have a safe position for area it is localized in, using localization position instead", robotId); + if (robot.CurrentArea?.Deck.DefaultLocalizationPose == null) + { + throw new SafeZoneException($"Robot with ID: {robotId} has no available safezone or localization poses in its current area"); + } + closestSafePosition = robot.CurrentArea.Deck.DefaultLocalizationPose.Pose; + } + // Cloning to avoid tracking same object var clonedPose = ObjectCopier.Clone(closestSafePosition); var customTaskQuery = new CustomTaskQuery @@ -471,12 +483,11 @@ private async Task StartMissionRun(MissionRun queuedMissionRun) logger.LogInformation("Started mission run '{Id}'", queuedMissionRun.Id); } - private static Pose ClosestSafePosition(Pose robotPose, IList safePositions) + private static Pose? ClosestSafePosition(Pose robotPose, IList safePositions) { if (safePositions == null || !safePositions.Any()) { - string message = "No safe position for area the robot is localized in"; - throw new SafeZoneException(message); + return null; } var closestPose = safePositions[0].Pose; From 9acc1381211c30fc66d3fb2bd554760cf1c184c1 Mon Sep 17 00:00:00 2001 From: andchiind Date: Tue, 15 Oct 2024 09:05:46 +0200 Subject: [PATCH 3/6] Use robot batteryStartMissionThreshold --- .../Models/UpdateRobotModelQuery.cs | 5 + backend/api/Database/Models/Robot.cs | 7 + backend/api/Database/Models/RobotModel.cs | 6 + backend/api/EventHandlers/MqttEventHandler.cs | 4 +- ...otMissionStartBatteryThreshold.Designer.cs | 1410 +++++++++++++++++ ...06_AddRobotMissionStartBatteryThreshold.cs | 28 + .../FlotillaDbContextModelSnapshot.cs | 3 + 7 files changed, 1461 insertions(+), 2 deletions(-) create mode 100644 backend/api/Migrations/20241015070506_AddRobotMissionStartBatteryThreshold.Designer.cs create mode 100644 backend/api/Migrations/20241015070506_AddRobotMissionStartBatteryThreshold.cs diff --git a/backend/api/Controllers/Models/UpdateRobotModelQuery.cs b/backend/api/Controllers/Models/UpdateRobotModelQuery.cs index 4fe777e01..ef2ce7ee7 100644 --- a/backend/api/Controllers/Models/UpdateRobotModelQuery.cs +++ b/backend/api/Controllers/Models/UpdateRobotModelQuery.cs @@ -16,5 +16,10 @@ public struct UpdateRobotModelQuery /// Lower pressure warning threshold in Bar /// public float? LowerPressureWarningThreshold { get; set; } + + /// + /// Lower battery threshold at which to allow missions to be scheduled, in percentage + /// + public float? BatteryMissionStartThreshold { get; set; } } } diff --git a/backend/api/Database/Models/Robot.cs b/backend/api/Database/Models/Robot.cs index 81bf5304e..e31b7115d 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -108,6 +108,13 @@ public bool IsRobotBatteryTooLow() return Model.BatteryWarningThreshold >= BatteryLevel; } + public bool IsRobotReadyToStartMissions() + { + if (IsRobotBatteryTooLow()) return false; + if (Model.BatteryMissionStartThreshold != null && Model.BatteryMissionStartThreshold > BatteryLevel) return false; + return !IsRobotPressureTooHigh() && !IsRobotPressureTooLow(); + } + public IList Documentation { get; set; } public IList VideoStreams { get; set; } diff --git a/backend/api/Database/Models/RobotModel.cs b/backend/api/Database/Models/RobotModel.cs index 8c0c9f2ff..09d61b4a2 100644 --- a/backend/api/Database/Models/RobotModel.cs +++ b/backend/api/Database/Models/RobotModel.cs @@ -59,6 +59,11 @@ public RobotModel(CreateRobotModelQuery query) /// public float? LowerPressureWarningThreshold { get; set; } + /// + /// Lower battery threshold at which to allow missions to be scheduled, in percentage + /// + public float? BatteryMissionStartThreshold { get; set; } + /// /// The average time in seconds spent by this model on a single tag (excluding recording duration for video/audio) /// @@ -69,6 +74,7 @@ public void Update(UpdateRobotModelQuery updateQuery) BatteryWarningThreshold = updateQuery.BatteryWarningThreshold; UpperPressureWarningThreshold = updateQuery.UpperPressureWarningThreshold; LowerPressureWarningThreshold = updateQuery.LowerPressureWarningThreshold; + BatteryMissionStartThreshold = updateQuery.BatteryMissionStartThreshold; } } } diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index 4d8bbf273..da69a1b82 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -412,7 +412,7 @@ private async void OnIsarBatteryUpdate(object? sender, MqttReceivedArgs mqttArgs _logger.LogInformation("Sending robot '{RobotName}' to its safe zone as its battery level is too low.", robot.Name); EmergencyActionService.SendRobotToSafezone(new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Recharging)); } - else if (robot.FlotillaStatus == RobotFlotillaStatus.Recharging && !(robot.IsRobotBatteryTooLow() || robot.IsRobotPressureTooHigh() || robot.IsRobotPressureTooLow())) + else if (robot.FlotillaStatus == RobotFlotillaStatus.Recharging && robot.IsRobotReadyToStartMissions()) { _logger.LogInformation("Releasing robot '{RobotName}' from its safe zone as its battery and pressure levels are good enough to run missions.", robot.Name); EmergencyActionService.ReleaseRobotFromSafezone(new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Normal)); @@ -431,7 +431,7 @@ private async void OnIsarPressureUpdate(object? sender, MqttReceivedArgs mqttArg _logger.LogInformation("Sending robot '{RobotName}' to its safe zone as its pressure is too low or high.", robot.Name); EmergencyActionService.SendRobotToSafezone(new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Recharging)); } - else if (robot.FlotillaStatus == RobotFlotillaStatus.Recharging && !(robot.IsRobotBatteryTooLow() || robot.IsRobotPressureTooHigh() || robot.IsRobotPressureTooLow())) + else if (robot.FlotillaStatus == RobotFlotillaStatus.Recharging && robot.IsRobotReadyToStartMissions()) { _logger.LogInformation("Releasing robot '{RobotName}' from its safe zone as its battery and pressure levels are good enough to run missions.", robot.Name); EmergencyActionService.ReleaseRobotFromSafezone(new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.Normal)); diff --git a/backend/api/Migrations/20241015070506_AddRobotMissionStartBatteryThreshold.Designer.cs b/backend/api/Migrations/20241015070506_AddRobotMissionStartBatteryThreshold.Designer.cs new file mode 100644 index 000000000..cdcd14651 --- /dev/null +++ b/backend/api/Migrations/20241015070506_AddRobotMissionStartBatteryThreshold.Designer.cs @@ -0,0 +1,1410 @@ +// +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("20241015070506_AddRobotMissionStartBatteryThreshold")] + partial class AddRobotMissionStartBatteryThreshold + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Api.Database.Models.AccessRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AccessLevel") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstallationId") + .HasColumnType("text"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.ToTable("AccessRoles"); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DeckId") + .IsRequired() + .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.Property("DockingEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("DefaultLocalizationPoses"); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AnalysisType") + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("InspectionType") + .IsRequired() + .HasColumnType("text"); + + b.Property("InspectionUrl") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("IsarStepId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionTaskId") + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoDuration") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("MissionTaskId"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionFinding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Finding") + .IsRequired() + .HasColumnType("text"); + + b.Property("InspectionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InspectionId") + .HasColumnType("text"); + + b.Property("IsarStepId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InspectionId"); + + b.ToTable("InspectionFindings"); + }); + + 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") + .IsRequired() + .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("LastSuccessfulRunId") + .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("LastSuccessfulRunId"); + + b.HasIndex("SourceId"); + + b.ToTable("MissionDefinitions"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AreaId") + .IsRequired() + .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("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("IsarMissionId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("MissionRunType") + .IsRequired() + .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.MissionTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsarTaskId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionRunId") + .HasColumnType("text"); + + b.Property("PoseId") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TagId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TagLink") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TaskOrder") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("MissionRunId"); + + b.ToTable("MissionTasks"); + }); + + 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("CurrentInstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentMissionId") + .HasColumnType("text"); + + b.Property("Deprecated") + .HasColumnType("boolean"); + + b.Property("FlotillaStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsarConnected") + .HasColumnType("boolean"); + + b.Property("IsarId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionQueueFrozen") + .HasColumnType("boolean"); + + 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("RobotCapabilities") + .HasColumnType("text"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrentAreaId"); + + b.HasIndex("CurrentInstallationId"); + + 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("BatteryMissionStartThreshold") + .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("CustomMissionTasks") + .HasColumnType("text"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("Api.Database.Models.UserInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Oid") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("Api.Database.Models.AccessRole", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId"); + + b.Navigation("Installation"); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.HasOne("Api.Database.Models.Deck", "Deck") + .WithMany() + .HasForeignKey("DeckId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.Inspection", b => + { + b.HasOne("Api.Database.Models.MissionTask", null) + .WithMany("Inspections") + .HasForeignKey("MissionTaskId"); + + b.OwnsOne("Api.Database.Models.Position", "InspectionTarget", b1 => + { + b1.Property("InspectionId") + .HasColumnType("text"); + + b1.Property("X") + .HasColumnType("real"); + + b1.Property("Y") + .HasColumnType("real"); + + b1.Property("Z") + .HasColumnType("real"); + + b1.HasKey("InspectionId"); + + b1.ToTable("Inspections"); + + b1.WithOwner() + .HasForeignKey("InspectionId"); + }); + + b.Navigation("InspectionTarget") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionFinding", b => + { + b.HasOne("Api.Database.Models.Inspection", null) + .WithMany("InspectionFindings") + .HasForeignKey("InspectionId"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.MissionRun", "LastSuccessfulRun") + .WithMany() + .HasForeignKey("LastSuccessfulRunId"); + + b.HasOne("Api.Database.Models.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Area"); + + b.Navigation("LastSuccessfulRun"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.Navigation("Area"); + + b.Navigation("Map"); + + b.Navigation("Robot"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionTask", b => + { + b.HasOne("Api.Database.Models.MissionRun", null) + .WithMany("Tasks") + .HasForeignKey("MissionRunId"); + + b.OwnsOne("Api.Database.Models.Pose", "RobotPose", b1 => + { + b1.Property("MissionTaskId") + .HasColumnType("text"); + + b1.HasKey("MissionTaskId"); + + b1.ToTable("MissionTasks"); + + b1.WithOwner() + .HasForeignKey("MissionTaskId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseMissionTaskId"); + + b2.ToTable("MissionTasks"); + + b2.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseMissionTaskId"); + + b2.ToTable("MissionTasks"); + + b2.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("RobotPose") + .IsRequired(); + }); + + 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.Installation", "CurrentInstallation") + .WithMany() + .HasForeignKey("CurrentInstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.RobotModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Api.Database.Models.DocumentInfo", "Documentation", 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("Url") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("Id"); + + b1.HasIndex("RobotId"); + + b1.ToTable("DocumentInfo"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + }); + + 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("CurrentInstallation"); + + b.Navigation("Documentation"); + + 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"); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.Navigation("InspectionFindings"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionTask", b => + { + b.Navigation("Inspections"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/api/Migrations/20241015070506_AddRobotMissionStartBatteryThreshold.cs b/backend/api/Migrations/20241015070506_AddRobotMissionStartBatteryThreshold.cs new file mode 100644 index 000000000..dd799164a --- /dev/null +++ b/backend/api/Migrations/20241015070506_AddRobotMissionStartBatteryThreshold.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class AddRobotMissionStartBatteryThreshold : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "BatteryMissionStartThreshold", + table: "RobotModels", + type: "real", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "BatteryMissionStartThreshold", + table: "RobotModels"); + } + } +} diff --git a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs index 6b30116c7..42f91af60 100644 --- a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs +++ b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs @@ -539,6 +539,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AverageDurationPerTag") .HasColumnType("real"); + b.Property("BatteryMissionStartThreshold") + .HasColumnType("real"); + b.Property("BatteryWarningThreshold") .HasColumnType("real"); From fbf96bfc830f5148d7821784e2c4759aab823e56 Mon Sep 17 00:00:00 2001 From: andchiind Date: Tue, 15 Oct 2024 13:56:59 +0200 Subject: [PATCH 4/6] Only reschedule missions if they did not complete --- backend/api/Services/MissionSchedulingService.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index 09d0ab66f..eac7c87c9 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -388,6 +388,14 @@ private async Task MoveInterruptedMissionsToQueue(IEnumerable interrupte return; } + var unfinishedTasks = missionRun.Tasks + .Where(t => !new List + {Database.Models.TaskStatus.Successful, Database.Models.TaskStatus.Failed} + .Contains(t.Status)) + .Select(t => new MissionTask(t)).ToList(); + + if (unfinishedTasks.Count == 0) continue; + var newMissionRun = new MissionRun { Name = missionRun.Name, @@ -397,11 +405,7 @@ private async Task MoveInterruptedMissionsToQueue(IEnumerable interrupte Area = missionRun.Area, Status = MissionStatus.Pending, DesiredStartTime = DateTime.UtcNow, - Tasks = missionRun.Tasks - .Where(t => !new List - {Database.Models.TaskStatus.Successful, Database.Models.TaskStatus.Failed} - .Contains(t.Status)) - .Select(t => new MissionTask(t)).ToList(), + Tasks = unfinishedTasks, Map = new MapMetadata() }; From d2a8bfea3ac33ee3c84cfee378d6963f29c3a23d Mon Sep 17 00:00:00 2001 From: andchiind Date: Tue, 15 Oct 2024 14:04:39 +0200 Subject: [PATCH 5/6] Catch exceptions when making calls to ISAR --- backend/api/Services/IsarService.cs | 21 +++++++++++++------ .../api/Services/MissionSchedulingService.cs | 4 ++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/backend/api/Services/IsarService.cs b/backend/api/Services/IsarService.cs index b5862e6fa..076be91fb 100644 --- a/backend/api/Services/IsarService.cs +++ b/backend/api/Services/IsarService.cs @@ -30,12 +30,21 @@ public async Task StartMission(Robot robot, MissionRun missionRun) mission_definition = new IsarMissionDefinition(missionRun, includeStartPose: missionRun.MissionRunType == MissionRunType.Localization) }; - var response = await CallApi( - HttpMethod.Post, - robot.IsarUri, - "schedule/start-mission", - missionDefinition - ); + HttpResponseMessage? response; + try + { + response = await CallApi( + HttpMethod.Post, + robot.IsarUri, + "schedule/start-mission", + missionDefinition + ); + } + catch (Exception e) + { + logger.LogError("Encountered an exception when making an API call to ISAR: {Message}", e.Message); + throw new IsarCommunicationException(e.Message); + } if (!response.IsSuccessStatusCode) { diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index eac7c87c9..a7150ccf5 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -151,11 +151,11 @@ or MissionRunNotFoundException { logger.LogError( ex, - "Mission run {MissionRunId} was not started successfully due to {ErrorMessage}", + "Mission run {MissionRunId} was not started successfully. {ErrorMessage}", missionRun.Id, ex.Message ); - await missionRunService.SetMissionRunToFailed(missionRun.Id, $"Mission run '{missionRun.Id}' was not started successfully due to '{ex.Message}'"); + await missionRunService.SetMissionRunToFailed(missionRun.Id, $"Mission run '{missionRun.Id}' was not started successfully. '{ex.Message}'"); } } From c4f283322c892377d1c5329747cd587aaf1058cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Tue, 29 Oct 2024 10:46:04 +0100 Subject: [PATCH 6/6] Give rerun missions unique names --- backend/api/Services/MissionSchedulingService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index a7150ccf5..638d45235 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -398,7 +398,7 @@ private async Task MoveInterruptedMissionsToQueue(IEnumerable interrupte var newMissionRun = new MissionRun { - Name = missionRun.Name, + Name = missionRun.Name + " unfinished tasks", Robot = missionRun.Robot, MissionRunType = missionRun.MissionRunType, InstallationCode = missionRun.Area!.Installation.InstallationCode,