Skip to content

Commit

Permalink
Add auto shceduling of missions
Browse files Browse the repository at this point in the history
  • Loading branch information
mrica-equinor committed Feb 20, 2025
1 parent a0a603f commit 8341505
Show file tree
Hide file tree
Showing 15 changed files with 537 additions and 4 deletions.
6 changes: 6 additions & 0 deletions backend/api.test/Mocks/MissionLoaderMock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ public async Task<List<MissionTask>> GetTasksForMission(string sourceMissionId)
return _mockMissionTasks;
}

public async Task<List<MissionTask>> GetTasksForMissionAsApp(string sourceMissionId)
{
await Task.Run(() => Thread.Sleep(1));
return _mockMissionTasks;
}

public async Task<List<PlantInfo>> GetPlantInfos()
{
await Task.Run(() => Thread.Sleep(1));
Expand Down
77 changes: 77 additions & 0 deletions backend/api/Database/Models/AutoScheduleFrequency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;

#pragma warning disable CS8618
namespace Api.Database.Models
{
[Owned]
public class AutoScheduleFrequency
{
[Required]
// In local time
public IList<TimeOnly> TimesOfDay { get; set; }

[Required]
public IList<DayOfWeek> DaysOfWeek { get; set; }

public void ValidateAutoScheduleFrequency()
{
if (TimesOfDay.Count == 0)
{
throw new ArgumentException(
"AutoScheduleFrequency must have at least one time of day"
);
}

if (DaysOfWeek.Count == 0)
{
throw new ArgumentException(
"AutoScheduleFrequency must have at least one day of week"
);
}
}

//
public IList<TimeSpan>? GetSchedulingTimesForNext24Hours()
{
// NCS is always in CET
TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById(
"Central European Standard Time"
);
DateTime nowLocal = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi);
TimeOnly nowLocalTimeOnly = new TimeOnly(
nowLocal.Hour,
nowLocal.Minute,
nowLocal.Second
);

var autoScheduleNext24Hours = new List<TimeSpan>();
if (DaysOfWeek.Contains(nowLocal.DayOfWeek))
{
foreach (TimeOnly time in TimesOfDay)
{
if (time > nowLocalTimeOnly)
{
autoScheduleNext24Hours.Add((time - nowLocalTimeOnly));
}
}
}
if (DaysOfWeek.Contains(nowLocal.DayOfWeek + 1))
{
foreach (TimeOnly time in TimesOfDay)
{
if (time < nowLocalTimeOnly)
{
autoScheduleNext24Hours.Add((time - nowLocalTimeOnly));
}
}
}
if (autoScheduleNext24Hours.Count > 0)
{
return autoScheduleNext24Hours;
}
return null;
}
}
}
2 changes: 2 additions & 0 deletions backend/api/Database/Models/MissionDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public class MissionDefinition : SortableRecord
[Column(TypeName = "bigint")]
public TimeSpan? InspectionFrequency { get; set; }

public AutoScheduleFrequency? AutoScheduleFrequency { get; set; }

public virtual MissionRun? LastSuccessfulRun { get; set; }

public InspectionArea? InspectionArea { get; set; }
Expand Down
5 changes: 4 additions & 1 deletion backend/api/Database/Models/MissionRun.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,10 @@ public MissionStatus Status
[Required]
public IList<MissionTask> Tasks
{
get => _tasks.OrderBy(t => t.TaskOrder).ToList();
get =>
_tasks != null
? _tasks.OrderBy(t => t.TaskOrder).ToList()
: new List<MissionTask>();
set => _tasks = value;
}

Expand Down
207 changes: 207 additions & 0 deletions backend/api/HostedServices/InspectionFrequencyHostedService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
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<InspectionFrequencyHostedService> _logger;
private readonly IServiceScopeFactory _scopeFactory;
private Timer? _timer = null;

public InspectionFrequencyHostedService(
ILogger<InspectionFrequencyHostedService> logger,
IServiceScopeFactory scopeFactory
)
{
_logger = logger;
_scopeFactory = scopeFactory;
}

private IMissionDefinitionService MissionDefinitionService =>
_scopeFactory
.CreateScope()
.ServiceProvider.GetRequiredService<IMissionDefinitionService>();

private IMissionSchedulingService MissionSchedulingService =>
_scopeFactory
.CreateScope()
.ServiceProvider.GetRequiredService<IMissionSchedulingService>();

private IRobotService RobotService =>
_scopeFactory.CreateScope().ServiceProvider.GetRequiredService<IRobotService>();

private IMissionLoader MissionLoader =>
_scopeFactory.CreateScope().ServiceProvider.GetRequiredService<IMissionLoader>();

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(10),
TimeSpan.FromMinutes(5)
);
return Task.CompletedTask;
}

private async void DoWork(object? state)
{
var missionQuery = new MissionDefinitionQueryStringParameters();

List<MissionDefinition>? missionDefinitions;
try
{
missionDefinitions = await MissionDefinitionService.ReadByHasInspectionFrequency();
}
catch (InvalidDataException e)
{
_logger.LogError(e, "{ErrorMessage}", e.Message);
return;
}

_logger.LogInformation("Mission definitions with inspection frequency found.");

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.LogError(
"No job delays 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.LogError(
"Mission definition {MissionDefinitionId} has no inspection area.",
missionDefinition.Id
);
return;
}

IList<Robot> 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.LogError(
"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.ScheduleMissionRunFromMissionDefinition(
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();
}
}
}
3 changes: 3 additions & 0 deletions backend/api/Migrations/FlotillaDbContextModelSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.IsRequired()
.HasColumnType("text");

b.Property<DateTime?>("StartDate")
.HasColumnType("timestamp with time zone");

b.HasKey("Id");

b.HasIndex("InspectionAreaId");
Expand Down
6 changes: 6 additions & 0 deletions backend/api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
using Api.Controllers;
using Api.Controllers.Models;
using Api.EventHandlers;
using Api.HostedServices;
using Api.Mqtt;
using Api.Options;
using Api.Services;
using Api.Services.ActionServices;
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;
Expand Down Expand Up @@ -107,10 +109,14 @@
builder.Services.AddHostedService<MqttService>();
builder.Services.AddHostedService<IsarConnectionEventHandler>();
builder.Services.AddHostedService<TeamsMessageEventHandler>();
builder.Services.AddHostedService<InspectionFrequencyHostedService>();

builder.Services.Configure<AzureAdOptions>(builder.Configuration.GetSection("AzureAd"));
builder.Services.Configure<MapBlobOptions>(builder.Configuration.GetSection("Maps"));

builder.Services.AddHangfire(Configuration => Configuration.UseInMemoryStorage());
builder.Services.AddHangfireServer();

builder
.Services.AddControllers()
.AddJsonOptions(options =>
Expand Down
Loading

0 comments on commit 8341505

Please sign in to comment.