diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs index 20f235073..3fb012a76 100644 --- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs +++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs @@ -468,8 +468,8 @@ public async Task LocalizationMissionCompletesAfterPressingSendToSafeZoneButton( Thread.Sleep(100); // Act - var eventArgs = new EmergencyButtonPressedForRobotEventArgs(robot.Id); - _emergencyActionService.RaiseEvent(nameof(EmergencyActionService.EmergencyButtonPressedForRobot), eventArgs); + var eventArgs = new RobotEmergencyEventArgs(robot.Id, RobotFlotillaStatus.SafeZone); + _emergencyActionService.RaiseEvent(nameof(EmergencyActionService.SendRobotToSafezoneTriggered), eventArgs); Thread.Sleep(1000); diff --git a/backend/api/Controllers/EmergencyActionController.cs b/backend/api/Controllers/EmergencyActionController.cs index a6c5911c1..72ae5a679 100644 --- a/backend/api/Controllers/EmergencyActionController.cs +++ b/backend/api/Controllers/EmergencyActionController.cs @@ -34,7 +34,7 @@ public async Task> AbortCurrentMissionAndSendAllRobotsToSaf foreach (var robot in robots) { - emergencyActionService.TriggerEmergencyButtonPressedForRobot(new EmergencyButtonPressedForRobotEventArgs(robot.Id)); + emergencyActionService.SendRobotToSafezone(new RobotEmergencyEventArgs(robot.Id, Database.Models.RobotFlotillaStatus.SafeZone)); } @@ -62,7 +62,7 @@ public async Task> ClearEmergencyStateForAllRobots( foreach (var robot in robots) { - emergencyActionService.TriggerEmergencyButtonDepressedForRobot(new EmergencyButtonPressedForRobotEventArgs(robot.Id)); + emergencyActionService.ReleaseRobotFromSafezone(new RobotEmergencyEventArgs(robot.Id, Database.Models.RobotFlotillaStatus.Normal)); } return NoContent(); diff --git a/backend/api/Controllers/Models/RobotResponse.cs b/backend/api/Controllers/Models/RobotResponse.cs index 86ea8c4d4..7d7115f4e 100644 --- a/backend/api/Controllers/Models/RobotResponse.cs +++ b/backend/api/Controllers/Models/RobotResponse.cs @@ -32,7 +32,7 @@ public class RobotResponse public bool Deprecated { get; set; } - public bool MissionQueueFrozen { get; set; } + public RobotFlotillaStatus FlotillaStatus { get; set; } public RobotStatus Status { get; set; } @@ -65,7 +65,7 @@ public RobotResponse(Robot robot) Port = robot.Port; IsarConnected = robot.IsarConnected; Deprecated = robot.Deprecated; - MissionQueueFrozen = robot.MissionQueueFrozen; + FlotillaStatus = robot.FlotillaStatus; Status = robot.Status; Pose = robot.Pose; CurrentMissionId = robot.CurrentMissionId; diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 477d893c4..acea70789 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -195,6 +195,8 @@ var inspection in task.Inspections.Where(inspection => !inspection.IsCompleted) public bool IsLocalizationMission() { return MissionRunType == MissionRunType.Localization; } public bool IsReturnHomeMission() { return Tasks is [{ Type: MissionTaskType.ReturnHome }]; } + + public bool IsEmergencyMission() { return MissionRunType == MissionRunType.Emergency; } } public enum MissionStatus diff --git a/backend/api/Database/Models/Robot.cs b/backend/api/Database/Models/Robot.cs index 626c6d147..c7020dc50 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -117,6 +117,9 @@ public bool IsRobotBatteryTooLow() [Required] public RobotStatus Status { get; set; } + [Required] + public RobotFlotillaStatus FlotillaStatus { get; set; } = RobotFlotillaStatus.Normal; + [Required] public Pose Pose { get; set; } @@ -146,6 +149,13 @@ public enum RobotStatus Blocked, } + public enum RobotFlotillaStatus + { + Normal, + SafeZone, + Recharging, + } + public enum RobotCapabilitiesEnum { take_thermal_image, diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 7929fcc62..fd9ea1ea8 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -43,8 +43,8 @@ public override void Subscribe() MissionRunService.MissionRunCreated += OnMissionRunCreated; MissionSchedulingService.RobotAvailable += OnRobotAvailable; MissionSchedulingService.LocalizationMissionSuccessful += OnLocalizationMissionSuccessful; - EmergencyActionService.EmergencyButtonPressedForRobot += OnEmergencyButtonPressedForRobot; - EmergencyActionService.EmergencyButtonDepressedForRobot += OnEmergencyButtonDepressedForRobot; + EmergencyActionService.SendRobotToSafezoneTriggered += OnSendRobotToSafezoneTriggered; + EmergencyActionService.ReleaseRobotFromSafezoneTriggered += OnReleaseRobotFromSafezoneTriggered; } public override void Unsubscribe() @@ -52,8 +52,8 @@ public override void Unsubscribe() MissionRunService.MissionRunCreated -= OnMissionRunCreated; MissionSchedulingService.RobotAvailable -= OnRobotAvailable; MissionSchedulingService.LocalizationMissionSuccessful -= OnLocalizationMissionSuccessful; - EmergencyActionService.EmergencyButtonPressedForRobot -= OnEmergencyButtonPressedForRobot; - EmergencyActionService.EmergencyButtonDepressedForRobot -= OnEmergencyButtonDepressedForRobot; + EmergencyActionService.SendRobotToSafezoneTriggered -= OnSendRobotToSafezoneTriggered; + EmergencyActionService.ReleaseRobotFromSafezoneTriggered -= OnReleaseRobotFromSafezoneTriggered; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -153,7 +153,7 @@ private async void OnLocalizationMissionSuccessful(object? sender, LocalizationM finally { _startMissionSemaphore.Release(); } } - private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e) + private async void OnSendRobotToSafezoneTriggered(object? sender, RobotEmergencyEventArgs e) { _logger.LogInformation("Triggered EmergencyButtonPressed event for robot ID: {RobotId}", e.RobotId); var robot = await RobotService.ReadById(e.RobotId); @@ -163,6 +163,19 @@ private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyBut 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); + return; + } + + try { await RobotService.UpdateFlotillaStatus(e.RobotId, e.RobotFlotillaStatus ?? RobotFlotillaStatus.Normal); } + catch (Exception ex) + { + _logger.LogError("Was not able to update Robot Flotilla status for robot {RobotId}, {ErrorMessage}", e.RobotId, ex.Message); + return; + } + try { await MissionScheduling.FreezeMissionRunQueueForRobot(e.RobotId); } catch (RobotNotFoundException) { return; } @@ -221,7 +234,7 @@ private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyBut finally { _startMissionSemaphore.Release(); } } - private async void OnEmergencyButtonDepressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e) + private async void OnReleaseRobotFromSafezoneTriggered(object? sender, RobotEmergencyEventArgs e) { _logger.LogInformation("Triggered EmergencyButtonPressed event for robot ID: {RobotId}", e.RobotId); var robot = await RobotService.ReadById(e.RobotId); @@ -231,9 +244,22 @@ private async void OnEmergencyButtonDepressedForRobot(object? sender, EmergencyB return; } + if (robot.FlotillaStatus == e.RobotFlotillaStatus) + { + _logger.LogInformation("Did not release robot from safezone since robot {RobotId} was already in the correct state", e.RobotId); + return; + } + try { await MissionScheduling.UnfreezeMissionRunQueueForRobot(e.RobotId); } catch (RobotNotFoundException) { return; } + try { await RobotService.UpdateFlotillaStatus(e.RobotId, e.RobotFlotillaStatus ?? RobotFlotillaStatus.Normal); } + catch (Exception ex) + { + _logger.LogError("Was not able to update Robot Flotilla status for robot {RobotId}, {ErrorMessage}", e.RobotId, ex.Message); + return; + } + _startMissionSemaphore.WaitOne(); try { await MissionScheduling.StartNextMissionRunIfSystemIsAvailable(robot.Id); } catch (MissionRunNotFoundException) { return; } @@ -268,8 +294,6 @@ private async void OnEmergencyButtonDepressedForRobot(object? sender, EmergencyB return localizationMission?.Area ?? null; } - _logger.LogError("Robot {RobotName} is not localized and no localization mission is ongoing.", robot.Name); - SignalRService.ReportSafeZoneFailureToSignalR(robot, $"Robot {robot.Name} has not been localised."); return null; } diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index 4066b192a..bc7c17f89 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -45,6 +45,7 @@ public MqttEventHandler(ILogger logger, IServiceScopeFactory s private ISignalRService SignalRService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private ITaskDurationService TaskDurationService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private ITeamsMessageService TeamsMessageService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IEmergencyActionService EmergencyActionService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); public override void Subscribe() { @@ -424,13 +425,39 @@ private async void OnIsarStepUpdate(object? sender, MqttReceivedArgs mqttArgs) private async void OnIsarBatteryUpdate(object? sender, MqttReceivedArgs mqttArgs) { var batteryStatus = (IsarBatteryMessage)mqttArgs.Message; - await BatteryTimeseriesService.AddBatteryEntry(batteryStatus.BatteryLevel, batteryStatus.IsarId); + var robot = await BatteryTimeseriesService.AddBatteryEntry(batteryStatus.BatteryLevel, batteryStatus.IsarId); + if (robot == null) return; + robot.BatteryLevel = batteryStatus.BatteryLevel; + + if (robot.FlotillaStatus == RobotFlotillaStatus.Normal && robot.IsRobotBatteryTooLow()) + { + _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())) + { + _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)); + } } private async void OnIsarPressureUpdate(object? sender, MqttReceivedArgs mqttArgs) { var pressureStatus = (IsarPressureMessage)mqttArgs.Message; - await PressureTimeseriesService.AddPressureEntry(pressureStatus.PressureLevel, pressureStatus.IsarId); + var robot = await PressureTimeseriesService.AddPressureEntry(pressureStatus.PressureLevel, pressureStatus.IsarId); + if (robot == null) return; + robot.PressureLevel = pressureStatus.PressureLevel; + + if (robot.FlotillaStatus == RobotFlotillaStatus.Normal && (robot.IsRobotPressureTooLow() || robot.IsRobotPressureTooHigh())) + { + _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())) + { + _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)); + } } private async void OnIsarPoseUpdate(object? sender, MqttReceivedArgs mqttArgs) diff --git a/backend/api/Migrations/20240724102244_AddRobotFlotillaStatus.Designer.cs b/backend/api/Migrations/20240724102244_AddRobotFlotillaStatus.Designer.cs new file mode 100644 index 000000000..f0beacf1c --- /dev/null +++ b/backend/api/Migrations/20240724102244_AddRobotFlotillaStatus.Designer.cs @@ -0,0 +1,1361 @@ +// +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("20240724102244_AddRobotFlotillaStatus")] + partial class AddRobotFlotillaStatus + { + /// + 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("EchoPoseId") + .HasColumnType("integer"); + + b.Property("EchoTagLink") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsarTaskId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionRunId") + .HasColumnType("text"); + + 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("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("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.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.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("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/20240724102244_AddRobotFlotillaStatus.cs b/backend/api/Migrations/20240724102244_AddRobotFlotillaStatus.cs new file mode 100644 index 000000000..1aa34aa5d --- /dev/null +++ b/backend/api/Migrations/20240724102244_AddRobotFlotillaStatus.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class AddRobotFlotillaStatus : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FlotillaStatus", + table: "Robots", + type: "text", + nullable: false, + defaultValue: ""); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "FlotillaStatus", + table: "Robots"); + } + } +} diff --git a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs index d5f381302..c089d596a 100644 --- a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs +++ b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs @@ -17,7 +17,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("ProductVersion", "8.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -454,6 +454,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Deprecated") .HasColumnType("boolean"); + b.Property("FlotillaStatus") + .IsRequired() + .HasColumnType("text"); + b.Property("Host") .IsRequired() .HasMaxLength(200) diff --git a/backend/api/Services/ActionServices/BatteryTimeseriesService.cs b/backend/api/Services/ActionServices/BatteryTimeseriesService.cs index 4bc7600e3..cc7cf7c2b 100644 --- a/backend/api/Services/ActionServices/BatteryTimeseriesService.cs +++ b/backend/api/Services/ActionServices/BatteryTimeseriesService.cs @@ -1,34 +1,40 @@ -namespace Api.Services.ActionServices +using Api.Database.Models; + +namespace Api.Services.ActionServices { public interface IBatteryTimeseriesService { - public Task AddBatteryEntry(float batteryLevel, string isarId); + public Task AddBatteryEntry(float batteryLevel, string isarId); } public class BatteryTimeseriesService(ILogger logger, IRobotService robotService) : IBatteryTimeseriesService { private const double Tolerance = 1E-05D; - public async Task AddBatteryEntry(float batteryLevel, string isarId) + public async Task AddBatteryEntry(float batteryLevel, string isarId) { var robot = await robotService.ReadByIsarId(isarId); if (robot == null) { logger.LogWarning("Could not find corresponding robot for battery update on robot with ISAR id'{IsarId}'", isarId); - return; + return null; } try { - if (Math.Abs(batteryLevel - robot.BatteryLevel) > Tolerance) await robotService.UpdateRobotBatteryLevel(robot.Id, batteryLevel); + if (Math.Abs(batteryLevel - robot.BatteryLevel) > Tolerance) + { + robot = await robotService.UpdateRobotBatteryLevel(robot.Id, batteryLevel); + } } catch (Exception e) { logger.LogWarning("Failed to update robot battery value for robot with ID '{isarId}'. Exception: {message}", isarId, e.Message); - return; + return null; } logger.LogDebug("Updated battery on robot '{RobotName}' with ISAR id '{IsarId}'", robot.Name, robot.IsarId); + return robot; } } } diff --git a/backend/api/Services/ActionServices/PressureTimeseriesService.cs b/backend/api/Services/ActionServices/PressureTimeseriesService.cs index ef5b79db9..a914b7054 100644 --- a/backend/api/Services/ActionServices/PressureTimeseriesService.cs +++ b/backend/api/Services/ActionServices/PressureTimeseriesService.cs @@ -1,34 +1,40 @@ -namespace Api.Services.ActionServices +using Api.Database.Models; + +namespace Api.Services.ActionServices { public interface IPressureTimeseriesService { - public Task AddPressureEntry(float pressureLevel, string isarId); + public Task AddPressureEntry(float pressureLevel, string isarId); } public class PressureTimeseriesService(ILogger logger, IRobotService robotService) : IPressureTimeseriesService { private const double Tolerance = 1E-05D; - public async Task AddPressureEntry(float pressureLevel, string isarId) + public async Task AddPressureEntry(float pressureLevel, string isarId) { var robot = await robotService.ReadByIsarId(isarId); if (robot == null) { logger.LogWarning("Could not find corresponding robot for pressure update on robot with ISAR id'{IsarId}'", isarId); - return; + return null; } try { - if (robot.PressureLevel is null || Math.Abs(pressureLevel - (float)robot.PressureLevel) > Tolerance) await robotService.UpdateRobotPressureLevel(robot.Id, pressureLevel); + if (robot.PressureLevel is null || Math.Abs(pressureLevel - (float)robot.PressureLevel) > Tolerance) + { + robot = await robotService.UpdateRobotPressureLevel(robot.Id, pressureLevel); + } } catch (Exception e) { logger.LogWarning("Failed to update robot pressure value for robot with ID '{isarId}'. Exception: {message}", isarId, e.Message); - return; + return null; } logger.LogDebug("Updated pressure on robot '{RobotName}' with ISAR id '{IsarId}'", robot.Name, robot.IsarId); + return robot; } } } diff --git a/backend/api/Services/EmergencyActionService.cs b/backend/api/Services/EmergencyActionService.cs index d35cb3856..06dc5a577 100644 --- a/backend/api/Services/EmergencyActionService.cs +++ b/backend/api/Services/EmergencyActionService.cs @@ -3,9 +3,9 @@ namespace Api.Services { public interface IEmergencyActionService { - public void TriggerEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobotEventArgs e); + public void SendRobotToSafezone(RobotEmergencyEventArgs e); - public void TriggerEmergencyButtonDepressedForRobot(EmergencyButtonPressedForRobotEventArgs e); + public void ReleaseRobotFromSafezone(RobotEmergencyEventArgs e); } public class EmergencyActionService : IEmergencyActionService @@ -15,28 +15,29 @@ public EmergencyActionService() { } - public void TriggerEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobotEventArgs e) + public void SendRobotToSafezone(RobotEmergencyEventArgs e) { - OnEmergencyButtonPressedForRobot(e); + OnSendRobotToSafezoneTriggered(e); } - public void TriggerEmergencyButtonDepressedForRobot(EmergencyButtonPressedForRobotEventArgs e) + public void ReleaseRobotFromSafezone(RobotEmergencyEventArgs e) { - OnEmergencyButtonDepressedForRobot(e); + OnReleaseRobotFromSafezoneTriggered(e); } - public static event EventHandler? EmergencyButtonPressedForRobot; + public static event EventHandler? SendRobotToSafezoneTriggered; - protected virtual void OnEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobotEventArgs e) + protected virtual void OnSendRobotToSafezoneTriggered(RobotEmergencyEventArgs e) { - EmergencyButtonPressedForRobot?.Invoke(this, e); + SendRobotToSafezoneTriggered?.Invoke(this, e); } - public static event EventHandler? EmergencyButtonDepressedForRobot; + public static event EventHandler? ReleaseRobotFromSafezoneTriggered; - protected virtual void OnEmergencyButtonDepressedForRobot(EmergencyButtonPressedForRobotEventArgs e) + protected virtual void OnReleaseRobotFromSafezoneTriggered(RobotEmergencyEventArgs e) { - EmergencyButtonDepressedForRobot?.Invoke(this, e); + ReleaseRobotFromSafezoneTriggered?.Invoke(this, e); } + } } diff --git a/backend/api/Services/Events/MissionEventArgs.cs b/backend/api/Services/Events/MissionEventArgs.cs index 9b75e6b9f..1deba4970 100644 --- a/backend/api/Services/Events/MissionEventArgs.cs +++ b/backend/api/Services/Events/MissionEventArgs.cs @@ -1,4 +1,6 @@ -namespace Api.Services.Events +using Api.Database.Models; + +namespace Api.Services.Events { public class MissionRunCreatedEventArgs(string missionRunId) : EventArgs { @@ -14,9 +16,10 @@ public class LocalizationMissionSuccessfulEventArgs(string robotId) : EventArgs public string RobotId { get; } = robotId; } - public class EmergencyButtonPressedForRobotEventArgs(string robotId) : EventArgs + public class RobotEmergencyEventArgs(string robotId, RobotFlotillaStatus? robotFlotillaStatus = null) : EventArgs { public string RobotId { get; } = robotId; + public RobotFlotillaStatus? RobotFlotillaStatus { get; } = robotFlotillaStatus; } public class TeamsMessageEventArgs(string teamsMessage) : EventArgs diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index fc43727fd..2b2656274 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -128,7 +128,7 @@ public async Task StartNextMissionRunIfSystemIsAvailable(string robotId) return; } - if ((robot.IsRobotPressureTooLow() || robot.IsRobotBatteryTooLow()) && !missionRun.IsReturnHomeMission()) + if ((robot.IsRobotPressureTooLow() || robot.IsRobotBatteryTooLow()) && !(missionRun.IsReturnHomeMission() || missionRun.IsEmergencyMission())) { missionRun = await HandleBatteryAndPressureLevel(robot); if (missionRun == null) { return; } @@ -381,13 +381,17 @@ private async Task MoveInterruptedMissionsToQueue(IEnumerable interrupte Area = missionRun.Area, Status = MissionStatus.Pending, DesiredStartTime = DateTime.UtcNow, - Tasks = missionRun.Tasks.Select(t => new MissionTask(t)).ToList(), + Tasks = missionRun.Tasks + .Where(t => !new List + {Database.Models.TaskStatus.Successful, Database.Models.TaskStatus.Failed} + .Contains(t.Status)) + .Select(t => new MissionTask(t)).ToList(), Map = new MapMetadata() }; try { - await missionRunService.Create(missionRun); + await missionRunService.Create(newMissionRun, triggerCreatedMissionRunEvent: false); } catch (UnsupportedRobotCapabilityException) { diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index f7316b753..0f29b88b3 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -28,6 +28,7 @@ public interface IRobotService public Task UpdateCurrentArea(string robotId, string? areaId); public Task UpdateDeprecated(string robotId, bool deprecated); public Task UpdateMissionQueueFrozen(string robotId, bool missionQueueFrozen); + public Task UpdateFlotillaStatus(string robotId, RobotFlotillaStatus status); public Task Delete(string id); public Task HandleLosingConnectionToIsar(string robotId); } @@ -275,6 +276,13 @@ public async Task UpdateCurrentArea(string robotId, string? areaId) public async Task UpdateMissionQueueFrozen(string robotId, bool missionQueueFrozen) { return await UpdateRobotProperty(robotId, "MissionQueueFrozen", missionQueueFrozen); } + public async Task UpdateFlotillaStatus(string robotId, RobotFlotillaStatus status) + { + var robot = await UpdateRobotProperty(robotId, "FlotillaStatus", status); + ThrowIfRobotIsNull(robot, robotId); + return robot; + } + public async Task> ReadAll() { return await GetRobotsWithSubModels().ToListAsync(); } public async Task ReadById(string id) { return await GetRobotsWithSubModels().FirstOrDefaultAsync(robot => robot.Id.Equals(id)); } diff --git a/frontend/src/components/Contexts/SafeZoneContext.tsx b/frontend/src/components/Contexts/SafeZoneContext.tsx index 3e67d76a3..938548171 100644 --- a/frontend/src/components/Contexts/SafeZoneContext.tsx +++ b/frontend/src/components/Contexts/SafeZoneContext.tsx @@ -4,6 +4,7 @@ import { useInstallationContext } from './InstallationContext' import { AlertType, useAlertContext } from './AlertContext' import { SafeZoneAlertContent } from 'components/Alerts/SafeZoneAlert' import { AlertCategory } from 'components/Alerts/AlertsBanner' +import { RobotFlotillaStatus } from 'models/Robot' interface ISafeZoneContext { safeZoneStatus: boolean @@ -32,8 +33,7 @@ export const SafeZoneProvider: FC = ({ children }) => { robot.currentInstallation.installationCode.toLocaleLowerCase() === installationCode.toLocaleLowerCase() ) - .map((robot) => robot.missionQueueFrozen) - .filter((status) => status === true) + .filter((robot) => robot.flotillaStatus === RobotFlotillaStatus.SafeZone) if (missionQueueFozenStatus.length > 0 && safeZoneStatus === false) { setSafeZoneStatus((oldStatus) => !oldStatus) diff --git a/frontend/src/components/Displays/RobotDisplays/RobotStatusIcon.tsx b/frontend/src/components/Displays/RobotDisplays/RobotStatusIcon.tsx index 0042ede4d..eb8323a2c 100644 --- a/frontend/src/components/Displays/RobotDisplays/RobotStatusIcon.tsx +++ b/frontend/src/components/Displays/RobotDisplays/RobotStatusIcon.tsx @@ -1,14 +1,14 @@ import { Icon, Typography } from '@equinor/eds-core-react' -import { RobotStatus } from 'models/Robot' +import { RobotFlotillaStatus, RobotStatus } from 'models/Robot' import { tokens } from '@equinor/eds-tokens' import { useLanguageContext } from 'components/Contexts/LanguageContext' -import { useSafeZoneContext } from 'components/Contexts/SafeZoneContext' import { Icons } from 'utils/icons' import { styled } from 'styled-components' interface StatusProps { status?: RobotStatus isarConnected: boolean + flotillaStatus?: RobotFlotillaStatus } const StyledStatus = styled.div` @@ -23,9 +23,8 @@ const StyledIcon = styled(Icon)` height: 24px; ` -export const RobotStatusChip = ({ status, isarConnected }: StatusProps) => { +export const RobotStatusChip = ({ status, flotillaStatus, isarConnected }: StatusProps) => { const { TranslateText } = useLanguageContext() - const { safeZoneStatus } = useSafeZoneContext() var iconColor = tokens.colors.text.static_icons__default.hex var statusIcon = Icons.CloudOff @@ -57,10 +56,14 @@ export const RobotStatusChip = ({ status, isarConnected }: StatusProps) => { iconColor = tokens.colors.interactive.disabled__text.hex statusIcon = Icons.Info status = RobotStatus.ConnectionIssues - } else if (safeZoneStatus) { + } else if (flotillaStatus && flotillaStatus === RobotFlotillaStatus.SafeZone) { iconColor = tokens.colors.interactive.danger__resting.hex statusIcon = Icons.Warning status = RobotStatus.SafeZone + } else if (flotillaStatus && flotillaStatus === RobotFlotillaStatus.Recharging) { + iconColor = '#FFC300' + statusIcon = Icons.BatteryCharging + status = RobotStatus.Recharging } return ( diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx index 091cccc0f..2928b995c 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx @@ -7,11 +7,12 @@ import { tokens } from '@equinor/eds-tokens' import { useMissionControlContext } from 'components/Contexts/MissionControlContext' import { BackendAPICaller } from 'api/ApiCaller' import { useInstallationContext } from 'components/Contexts/InstallationContext' -import { useSafeZoneContext } from 'components/Contexts/SafeZoneContext' import { TaskType } from 'models/Task' import { AlertType, useAlertContext } from 'components/Contexts/AlertContext' import { FailedRequestAlertContent } from 'components/Alerts/FailedRequestAlert' import { AlertCategory } from 'components/Alerts/AlertsBanner' +import { useRobotContext } from 'components/Contexts/RobotContext' +import { RobotFlotillaStatus } from 'models/Robot' const StyledDisplayButtons = styled.div` display: flex; @@ -135,11 +136,18 @@ export const StopMissionDialog = ({ missionName, robotId, missionTaskType }: Mis export const StopRobotDialog = (): JSX.Element => { const [isStopRobotDialogOpen, setIsStopRobotDialogOpen] = useState(false) - const { safeZoneStatus } = useSafeZoneContext() + const { enabledRobots } = useRobotContext() const { TranslateText } = useLanguageContext() const { installationCode } = useInstallationContext() const { setAlert } = useAlertContext() + const safeZoneActivated = + enabledRobots.find( + (r) => + r.currentInstallation.installationCode === installationCode && + r.flotillaStatus === RobotFlotillaStatus.SafeZone + ) !== undefined + const openDialog = async () => { setIsStopRobotDialogOpen(true) } @@ -176,7 +184,7 @@ export const StopRobotDialog = (): JSX.Element => { return ( <> - {!safeZoneStatus ? ( + {!safeZoneActivated ? ( <>{TranslateText('Send robots to safe zone')} ) : ( <>{TranslateText('Dismiss robots from safe zone')} @@ -186,7 +194,7 @@ export const StopRobotDialog = (): JSX.Element => { - {!safeZoneStatus + {!safeZoneActivated ? TranslateText('Send robots to safe zone') + '?' : TranslateText('Dismiss robots from safe zone') + '?'} @@ -195,12 +203,12 @@ export const StopRobotDialog = (): JSX.Element => { - {!safeZoneStatus + {!safeZoneActivated ? TranslateText('Send robots to safe zone long text') : TranslateText('Dismiss robots from safe zone long text')} - {!safeZoneStatus + {!safeZoneActivated ? TranslateText('Send robots to safe confirmation text') : TranslateText('Dismiss robots from safe confirmation text')} @@ -217,7 +225,7 @@ export const StopRobotDialog = (): JSX.Element => { > {TranslateText('Cancel')} - {!safeZoneStatus ? ( + {!safeZoneActivated ? ( diff --git a/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusCard.tsx b/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusCard.tsx index 5369821b1..2a0c915d2 100644 --- a/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusCard.tsx +++ b/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusCard.tsx @@ -93,7 +93,11 @@ export const RobotStatusCard = ({ robot }: RobotProps) => { {TranslateText('Status')} - + {robot.status !== RobotStatus.Offline ? ( diff --git a/frontend/src/components/Pages/RobotPage/RobotPage.tsx b/frontend/src/components/Pages/RobotPage/RobotPage.tsx index 9df7425b9..232257249 100644 --- a/frontend/src/components/Pages/RobotPage/RobotPage.tsx +++ b/frontend/src/components/Pages/RobotPage/RobotPage.tsx @@ -91,6 +91,7 @@ export const RobotPage = () => { diff --git a/frontend/src/language/en.json b/frontend/src/language/en.json index afdadbd8a..f944420dd 100644 --- a/frontend/src/language/en.json +++ b/frontend/src/language/en.json @@ -257,5 +257,6 @@ "Failed to release robots from safe zone": "Failed to release robots from safe zone", "Failed to send robot {0} home": "Failed to send robot {0} to starting position", "Failed to update inspection": "Failed to update inspection", - "Battery": "Battery" + "Battery": "Battery", + "Recharging": "Recharging" } diff --git a/frontend/src/language/no.json b/frontend/src/language/no.json index 3c7b75c5e..22b6121ee 100644 --- a/frontend/src/language/no.json +++ b/frontend/src/language/no.json @@ -257,5 +257,6 @@ "Failed to release robots from safe zone": "Kunne ikke slippe roboter ut av trygg sone", "Failed to send robot {0} home": "Kunne ikke sende robot {0} til startposisjon", "Failed to update inspection": "Kunne ikke oppdatere inspeksjon", - "Battery": "Batteri" + "Battery": "Batteri", + "Recharging": "Lader" } diff --git a/frontend/src/models/Robot.ts b/frontend/src/models/Robot.ts index 9764f5ee6..2a28c15b2 100644 --- a/frontend/src/models/Robot.ts +++ b/frontend/src/models/Robot.ts @@ -11,9 +11,16 @@ export enum RobotStatus { Offline = 'Offline', Blocked = 'Blocked', SafeZone = 'Safe Zone', + Recharging = 'Recharging', ConnectionIssues = 'Connection Issues', } +export enum RobotFlotillaStatus { + Normal = 'Normal', + SafeZone = 'SafeZone', + Recharging = 'Recharging', +} + export interface Robot { id: string name?: string @@ -33,7 +40,7 @@ export interface Robot { videoStreams?: VideoStream[] isarUri?: string currentArea?: Area - missionQueueFrozen?: boolean + flotillaStatus?: RobotFlotillaStatus } export const placeholderRobot: Robot = { id: 'placeholderRobotId',