From 3b4f419ffe9ad99fcde9aa05a21b07d2496722b0 Mon Sep 17 00:00:00 2001 From: mrica-equinor Date: Fri, 21 Feb 2025 15:06:10 +0100 Subject: [PATCH] Add auto scheduling of missions --- backend/api/Database/Models/MissionRun.cs | 5 +- .../InspectionFrequencyHostedService.cs | 206 ++++++++++++++++++ backend/api/Program.cs | 6 + .../api/Services/MissionDefinitionService.cs | 13 ++ .../api/Services/MissionSchedulingService.cs | 161 +++++++++++++- backend/api/api.csproj | 5 +- 6 files changed, 392 insertions(+), 4 deletions(-) create mode 100644 backend/api/HostedServices/InspectionFrequencyHostedService.cs diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 4af3c8a7..e8a96f0c 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -50,7 +50,10 @@ public MissionStatus Status [Required] public IList Tasks { - get => _tasks.OrderBy(t => t.TaskOrder).ToList(); + get => + _tasks != null + ? _tasks.OrderBy(t => t.TaskOrder).ToList() + : new List(); set => _tasks = value; } diff --git a/backend/api/HostedServices/InspectionFrequencyHostedService.cs b/backend/api/HostedServices/InspectionFrequencyHostedService.cs new file mode 100644 index 00000000..ed386d29 --- /dev/null +++ b/backend/api/HostedServices/InspectionFrequencyHostedService.cs @@ -0,0 +1,206 @@ +using System; +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Api.Services.MissionLoaders; +using Api.Utilities; +using Hangfire; + +namespace Api.HostedServices +{ + public class InspectionFrequencyHostedService : IHostedService, IDisposable + { + private readonly ILogger _logger; + private readonly IServiceScopeFactory _scopeFactory; + private Timer? _timer = null; + + public InspectionFrequencyHostedService( + ILogger logger, + IServiceScopeFactory scopeFactory + ) + { + _logger = logger; + _scopeFactory = scopeFactory; + } + + private IMissionDefinitionService MissionDefinitionService => + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); + + private IMissionSchedulingService MissionSchedulingService => + _scopeFactory + .CreateScope() + .ServiceProvider.GetRequiredService(); + + private IRobotService RobotService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + + private IMissionLoader MissionLoader => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + + public Task StartAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Inspection Frequency Hosted Service Running."); + + var timeUntilMidnight = (DateTime.UtcNow.AddDays(1) - DateTime.UtcNow).TotalSeconds; + _timer = new Timer( + DoWork, + null, + TimeSpan.FromSeconds(timeUntilMidnight), + TimeSpan.FromDays(1) + ); + return Task.CompletedTask; + } + + private async void DoWork(object? state) + { + var missionQuery = new MissionDefinitionQueryStringParameters(); + + List? missionDefinitions; + try + { + missionDefinitions = await MissionDefinitionService.ReadByHasInspectionFrequency(); + } + catch (InvalidDataException e) + { + _logger.LogError(e, "{ErrorMessage}", e.Message); + return; + } + + if (missionDefinitions == null) + { + _logger.LogInformation("No mission definitions with inspection frequency found."); + return; + } + + var selectedMissionDefinitions = missionDefinitions.Where(m => + m.AutoScheduleFrequency != null + && m.AutoScheduleFrequency.GetSchedulingTimesForNext24Hours() != null + ); + + if (selectedMissionDefinitions.Any() == false) + { + _logger.LogInformation( + "No mission definitions with inspection frequency found that are due for inspection today." + ); + return; + } + + foreach (var missionDefinition in selectedMissionDefinitions) + { + var jobDelays = + missionDefinition.AutoScheduleFrequency!.GetSchedulingTimesForNext24Hours(); + + if (jobDelays == null) + { + _logger.LogWarning( + "No job schedules found for mission definition {MissionDefinitionId}.", + missionDefinition.Id + ); + return; + } + + foreach (var jobDelay in jobDelays) + { + _logger.LogInformation( + "Scheduling mission run for mission definition {MissionDefinitionId} in {TimeLeft}.", + missionDefinition.Id, + jobDelay + ); + BackgroundJob.Schedule( + () => AutomaticScheduleMissionRun(missionDefinition), + jobDelay + ); + } + } + } + + public async Task AutomaticScheduleMissionRun(MissionDefinition missionDefinition) + { + _logger.LogInformation( + "Scheduling mission run for mission definition {MissionDefinitionId}.", + missionDefinition.Id + ); + + if (missionDefinition.InspectionArea == null) + { + _logger.LogWarning( + "Mission definition {MissionDefinitionId} has no inspection area.", + missionDefinition.Id + ); + return; + } + + IList robots; + try + { + robots = await RobotService.ReadRobotsForInstallation( + missionDefinition.InstallationCode + ); + } + catch (Exception e) + { + _logger.LogError(e, "{ErrorMessage}", e.Message); + return; + } + + if (robots == null) + { + _logger.LogInformation( + "No robots found for installation code {InstallationCode}.", + missionDefinition.InstallationCode + ); + return; + } + + var robot = robots.FirstOrDefault(r => + r.CurrentInspectionArea?.Id == missionDefinition.InspectionArea.Id + ); + if (robot == null) + { + _logger.LogWarning( + "No robot found for mission definition {MissionDefinitionId} and inspection area {InspectionAreaId}.", + missionDefinition.Id, + missionDefinition.InspectionArea.Id + ); + return; + } + + _logger.LogInformation( + "Scheduling mission run for mission definition {MissionDefinitionId} and robot {RobotId}.", + missionDefinition.Id, + robot.Id + ); + + try + { + await MissionSchedulingService.ScheduleMissionRunFromMissionDefinitionLastSuccessfullRun( + missionDefinition.Id, + robot.Id + ); + } + catch (Exception e) + { + _logger.LogError(e, "{ErrorMessage}", e.Message); + return; + } + + return; + } + + public Task StopAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Inspection Frequency Hosted Service is stopping."); + + _timer?.Change(Timeout.Infinite, 0); + + return Task.CompletedTask; + } + + public void Dispose() + { + _timer?.Dispose(); + } + } +} diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 7d5dc965..4386e39d 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -3,6 +3,7 @@ using Api.Controllers; using Api.Controllers.Models; using Api.EventHandlers; +using Api.HostedServices; using Api.Mqtt; using Api.Options; using Api.Services; @@ -10,6 +11,7 @@ using Api.SignalRHubs; using Api.Utilities; using Azure.Identity; +using Hangfire; using Microsoft.ApplicationInsights.Extensibility.Implementation; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Http.Connections; @@ -106,10 +108,14 @@ builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); +builder.Services.AddHostedService(); builder.Services.Configure(builder.Configuration.GetSection("AzureAd")); builder.Services.Configure(builder.Configuration.GetSection("Maps")); +builder.Services.AddHangfire(Configuration => Configuration.UseInMemoryStorage()); +builder.Services.AddHangfireServer(); + builder .Services.AddControllers() .AddJsonOptions(options => diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs index 7775f994..2791390c 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -26,6 +26,8 @@ public Task> ReadByInspectionAreaId( bool readOnly = true ); + public Task?> ReadByHasInspectionFrequency(bool readOnly = true); + public Task?> GetTasksFromSource(Source source); public Task> ReadBySourceId(string sourceId, bool readOnly = true); @@ -145,6 +147,17 @@ public async Task> ReadBySourceId( .ToListAsync(); } + public async Task?> ReadByHasInspectionFrequency( + bool readOnly = true + ) + { + var missions = await GetMissionDefinitionsWithSubModels(readOnly: readOnly) + .Where(m => m.IsDeprecated == false && m.AutoScheduleFrequency != null) + .ToListAsync(); + + return missions; + } + public async Task UpdateLastSuccessfulMissionRun( string missionRunId, string missionDefinitionId diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs index e456653f..5a9eb3a7 100644 --- a/backend/api/Services/MissionSchedulingService.cs +++ b/backend/api/Services/MissionSchedulingService.cs @@ -28,17 +28,25 @@ public interface IMissionSchedulingService public void TriggerRobotAvailable(RobotAvailableEventArgs e); public Task AbortActiveReturnToHomeMission(string robotId); + + public Task ScheduleMissionRunFromMissionDefinitionLastSuccessfullRun( + string missionDefinitionId, + string robotId + ); } public class MissionSchedulingService( ILogger logger, IMissionRunService missionRunService, + IMissionDefinitionService missionDefinitionService, + IMapService mapService, IRobotService robotService, IIsarService isarService, IReturnToHomeService returnToHomeService, ISignalRService signalRService, IErrorHandlingService errorHandlingService, - IInspectionAreaService inspectionAreaService + IInspectionAreaService inspectionAreaService, + IInstallationService installationService ) : IMissionSchedulingService { public async Task StartNextMissionRunIfSystemIsAvailable(Robot robot) @@ -674,5 +682,156 @@ protected virtual void OnRobotAvailable(RobotAvailableEventArgs e) } public static event EventHandler? RobotAvailable; + + public async Task ScheduleMissionRunFromMissionDefinitionLastSuccessfullRun( + string missionDefinitionId, + string robotId + ) + { + logger.LogInformation( + "Scheduling mission run for robot with ID {RobotId} from mission definition with ID {MissionDefinitionId}", + robotId, + missionDefinitionId + ); + + Robot robot; + try + { + robot = await robotService.GetRobotWithSchedulingPreCheck(robotId); + } + catch (Exception e) when (e is RobotNotFoundException) + { + logger.LogError( + "Robot with ID {RobotId} was not found when scheduling mission run", + robotId + ); + return; + } + catch (Exception e) when (e is RobotPreCheckFailedException) + { + logger.LogError( + "Robot with ID {RobotId} failed pre-check when scheduling mission run", + robotId + ); + return; + } + + var missionDefinition = await missionDefinitionService.ReadById( + missionDefinitionId, + readOnly: true + ); + if (missionDefinition == null) + { + logger.LogWarning( + "Mission definition with ID {MissionDefinitionId} was not found", + missionDefinitionId + ); + return; + } + else if (missionDefinition.InspectionArea == null) + { + logger.LogWarning( + "Mission definition with ID {id} does not have an inspection area when scheduling", + missionDefinition.Id + ); + return; + } + else if (missionDefinition.LastSuccessfulRun == null) + { + logger.LogWarning( + "Mission definition with ID {id} does not have a last successful run when scheduling", + missionDefinition.Id + ); + return; + } + + try + { + await installationService.AssertRobotIsOnSameInstallationAsMission( + robot, + missionDefinition + ); + } + catch (InstallationNotFoundException) + { + logger.LogError( + "Installation for mission definition with ID {MissionDefinitionId} was not found", + missionDefinitionId + ); + return; + } + catch (RobotNotInSameInstallationAsMissionException) + { + logger.LogError( + "Robot with ID {RobotId} is not in the same installation as the mission definition with ID {MissionDefinitionId}", + robotId, + missionDefinitionId + ); + return; + } + + var missionTasks = new List(); + + foreach (var task in missionDefinition.LastSuccessfulRun.Tasks) + { + missionTasks.Add(new MissionTask(task)); + } + + if (missionTasks.Count == 0) + { + logger.LogWarning( + "Mission definition with ID {id} does not have tasks when scheduling", + missionDefinition.Id + ); + return; + } + + var missionRun = new MissionRun + { + Name = missionDefinition.Name, + Robot = robot, + MissionId = missionDefinition.Id, + Status = MissionStatus.Pending, + MissionRunType = MissionRunType.Normal, + DesiredStartTime = DateTime.UtcNow, + Tasks = missionTasks, + InstallationCode = missionDefinition.InstallationCode, + InspectionArea = missionDefinition.InspectionArea, + }; + + if (missionDefinition.Map == null) + { + var newMap = await mapService.ChooseMapFromMissionRunTasks(missionRun); + if (newMap != null) + { + logger.LogInformation( + $"Assigned map {newMap.MapName} to mission definition with id {missionDefinition.Id}" + ); + missionDefinition.Map = newMap; + await missionDefinitionService.Update(missionDefinition); + } + } + + if (missionRun.Tasks.Any()) + { + missionRun.SetEstimatedTaskDuration(); + } + + MissionRun newMissionRun; + try + { + newMissionRun = await missionRunService.Create(missionRun); + } + catch (UnsupportedRobotCapabilityException) + { + logger.LogError( + "Unsupported robot capability detected when scheduling mission for robot {robotName}.", + robot.Name + ); + return; + } + + return; + } } } diff --git a/backend/api/api.csproj b/backend/api/api.csproj index a14b3186..d99d24ab 100644 --- a/backend/api/api.csproj +++ b/backend/api/api.csproj @@ -14,8 +14,10 @@ - + + + @@ -23,7 +25,6 @@ all runtime; build; native; contentfiles; analyzers -