diff --git a/backend/api.test/EndpointTest.cs b/backend/api.test/EndpointTest.cs
index e90269c1d..6fa066256 100644
--- a/backend/api.test/EndpointTest.cs
+++ b/backend/api.test/EndpointTest.cs
@@ -558,27 +558,13 @@ public async Task SafePositionTest()
var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions);
Assert.True(area != null);
- // Arrange - Get a Robot
- string url = "/robots";
- var robotResponse = await _client.GetAsync(url);
- Assert.True(robotResponse.IsSuccessStatusCode);
- var robots = await robotResponse.Content.ReadFromJsonAsync>(_serializerOptions);
- Assert.True(robots != null);
- var robot = robots[0];
- string robotId = robot.Id;
-
// Act
- string goToSafePositionUrl = $"/robots/{robotId}/{testInstallation}/{testArea}/go-to-safe-position";
+ string goToSafePositionUrl = $"/emergency-action/{testInstallation}/abort-current-missions-and-send-all-robots-to-safe-zone";
var missionResponse = await _client.PostAsync(goToSafePositionUrl, null);
// Assert
Assert.True(missionResponse.IsSuccessStatusCode);
- var missionRun = await missionResponse.Content.ReadFromJsonAsync(_serializerOptions);
- Assert.True(missionRun != null);
- Assert.True(
- JsonSerializer.Serialize(missionRun.Tasks[0].RobotPose.Position) ==
- JsonSerializer.Serialize(testPosition)
- );
+
}
[Fact]
diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs
index 8ab6956e6..14c315110 100644
--- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs
+++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs
@@ -47,6 +47,14 @@ public class TestMissionEventHandler : IDisposable
private readonly RobotControllerMock _robotControllerMock;
private readonly IRobotModelService _robotModelService;
private readonly IRobotService _robotService;
+ private readonly IMissionScheduling _missionScheduling;
+ private readonly IIsarService _isarServiceMock;
+ private readonly IInstallationService _installationService;
+ private readonly IDefaultLocalizationPoseService _defaultLocalisationPoseService;
+ private readonly IPlantService _plantService;
+ private readonly IDeckService _deckService;
+ private readonly IAreaService _areaService;
+ private readonly IMissionSchedulingService _missionSchedulingService;
public TestMissionEventHandler(DatabaseFixture fixture)
{
@@ -54,6 +62,8 @@ public TestMissionEventHandler(DatabaseFixture fixture)
var mqttServiceLogger = new Mock>().Object;
var mqttEventHandlerLogger = new Mock>().Object;
var missionLogger = new Mock>().Object;
+ var missionSchedulingLogger = new Mock>().Object;
+ var missionSchedulingServiceLogger = new Mock>().Object;
var configuration = WebApplication.CreateBuilder().Configuration;
@@ -65,6 +75,14 @@ public TestMissionEventHandler(DatabaseFixture fixture)
_robotService = new RobotService(_context, _robotModelService);
_robotModelService = new RobotModelService(_context);
_robotControllerMock = new RobotControllerMock();
+ _isarServiceMock = new MockIsarService();
+ _installationService = new InstallationService(_context);
+ _defaultLocalisationPoseService = new DefaultLocalizationPoseService(_context);
+ _plantService = new PlantService(_context, _installationService);
+ _deckService = new DeckService(_context, _defaultLocalisationPoseService, _installationService, _plantService);
+ _areaService = new AreaService(_context, _installationService, _plantService, _deckService, _defaultLocalisationPoseService);
+ _missionSchedulingService = new MissionSchedulingService(missionSchedulingServiceLogger, _missionRunService, _robotService, _robotControllerMock.Mock.Object);
+ _missionScheduling = new MissionScheduling(missionSchedulingLogger, _missionRunService, _isarServiceMock, _robotService, _areaService, _missionSchedulingService);
var mockServiceProvider = new Mock();
@@ -75,6 +93,9 @@ public TestMissionEventHandler(DatabaseFixture fixture)
mockServiceProvider
.Setup(p => p.GetService(typeof(IRobotService)))
.Returns(_robotService);
+ mockServiceProvider
+ .Setup(p => p.GetService(typeof(IMissionScheduling)))
+ .Returns(_missionScheduling);
mockServiceProvider
.Setup(p => p.GetService(typeof(RobotController)))
.Returns(_robotControllerMock.Mock.Object);
@@ -119,6 +140,7 @@ public TestMissionEventHandler(DatabaseFixture fixture)
{
Name = "testMission",
MissionId = Guid.NewGuid().ToString(),
+ MissionRunPriority = MissionRunPriority.Normal,
Status = MissionStatus.Pending,
DesiredStartTime = DateTimeOffset.Now,
Area = NewArea,
diff --git a/backend/api.test/Mocks/RobotControllerMock.cs b/backend/api.test/Mocks/RobotControllerMock.cs
index 591dc9f05..33d097a2d 100644
--- a/backend/api.test/Mocks/RobotControllerMock.cs
+++ b/backend/api.test/Mocks/RobotControllerMock.cs
@@ -13,7 +13,6 @@ internal class RobotControllerMock
public Mock MissionServiceMock;
public Mock Mock;
public Mock AreaServiceMock;
- public Mock EchoServiceMock;
public RobotControllerMock()
{
@@ -22,7 +21,6 @@ public RobotControllerMock()
RobotServiceMock = new Mock();
RobotModelServiceMock = new Mock();
AreaServiceMock = new Mock();
- EchoServiceMock = new Mock();
var mockLoggerController = new Mock>();
@@ -32,8 +30,7 @@ public RobotControllerMock()
IsarServiceMock.Object,
MissionServiceMock.Object,
RobotModelServiceMock.Object,
- AreaServiceMock.Object,
- EchoServiceMock.Object
+ AreaServiceMock.Object
)
{
CallBase = true
diff --git a/backend/api/Controllers/EmergencyActionController.cs b/backend/api/Controllers/EmergencyActionController.cs
new file mode 100644
index 000000000..797c746f6
--- /dev/null
+++ b/backend/api/Controllers/EmergencyActionController.cs
@@ -0,0 +1,85 @@
+using System.Globalization;
+using Api.Controllers.Models;
+using Api.Services;
+using Api.Services.Events;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+namespace Api.Controllers
+{
+ [ApiController]
+ [Route("emergency-action")]
+ public class EmergencyActionController : ControllerBase
+ {
+ private readonly IEmergencyActionService _emergencyActionService;
+ private readonly IRobotService _robotService;
+
+ public EmergencyActionController(IRobotService robotService, IEmergencyActionService emergencyActionService)
+ {
+ _robotService = robotService;
+ _emergencyActionService = emergencyActionService;
+ }
+
+ ///
+ /// This endpoint will abort the current running mission run and attempt to return the robot to a safe position in the
+ /// area. The mission run queue for the robot will be frozen and no further missions will run until the emergency
+ /// action has been reversed.
+ ///
+ ///
+ /// The endpoint fires an event which is then processed to stop the robot and schedule the next mission
+ ///
+ [HttpPost]
+ [Route("{installationCode}/abort-current-missions-and-send-all-robots-to-safe-zone")]
+ [Authorize(Roles = Role.User)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public ActionResult AbortCurrentMissionAndSendAllRobotsToSafeZone(
+ [FromRoute] string installationCode)
+ {
+
+ var robots = _robotService.ReadAll().Result.ToList().FindAll(a =>
+ a.CurrentInstallation.ToLower(CultureInfo.CurrentCulture).Equals(installationCode.ToLower(CultureInfo.CurrentCulture), StringComparison.Ordinal) &&
+ a.CurrentArea != null);
+
+ foreach (var robot in robots)
+ {
+ _emergencyActionService.TriggerEmergencyButtonPressedForRobot(new EmergencyButtonPressedForRobotEventArgs(robot.Id));
+
+ }
+
+ return NoContent();
+
+ }
+
+ ///
+ /// This query will clear the emergency state that is introduced by aborting the current mission and returning to a
+ /// safe zone. Clearing the emergency state means that mission runs that may be in the robots queue will start."
+ ///
+ [HttpPost]
+ [Route("{installationCode}/clear-emergency-state")]
+ [Authorize(Roles = Role.User)]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public ActionResult ClearEmergencyStateForAllRobots(
+ [FromRoute] string installationCode)
+ {
+ var robots = _robotService.ReadAll().Result.ToList().FindAll(a =>
+ a.CurrentInstallation.ToLower(CultureInfo.CurrentCulture).Equals(installationCode.ToLower(CultureInfo.CurrentCulture), StringComparison.Ordinal) &&
+ a.CurrentArea != null);
+
+ foreach (var robot in robots)
+ {
+ _emergencyActionService.TriggerEmergencyButtonDepressedForRobot(new EmergencyButtonPressedForRobotEventArgs(robot.Id));
+ }
+
+ return NoContent();
+ }
+ }
+}
diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs
index df3398f2e..9c2620c9e 100644
--- a/backend/api/Controllers/MissionSchedulingController.cs
+++ b/backend/api/Controllers/MissionSchedulingController.cs
@@ -19,7 +19,7 @@ public class MissionSchedulingController : ControllerBase
private readonly IMapService _mapService;
private readonly IMissionDefinitionService _missionDefinitionService;
private readonly IMissionRunService _missionRunService;
- private readonly IMissionSchedulingService _missionSchedulingService;
+ private readonly ICustomMissionSchedulingService _customMissionSchedulingService;
private readonly IRobotService _robotService;
private readonly ISourceService _sourceService;
private readonly IStidService _stidService;
@@ -35,7 +35,7 @@ public MissionSchedulingController(
IMapService mapService,
IStidService stidService,
ISourceService sourceService,
- IMissionSchedulingService missionSchedulingService
+ ICustomMissionSchedulingService customMissionSchedulingService
)
{
_missionDefinitionService = missionDefinitionService;
@@ -48,7 +48,7 @@ IMissionSchedulingService missionSchedulingService
_stidService = stidService;
_sourceService = sourceService;
_missionDefinitionService = missionDefinitionService;
- _missionSchedulingService = missionSchedulingService;
+ _customMissionSchedulingService = customMissionSchedulingService;
_logger = logger;
}
@@ -96,6 +96,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery
Robot = robot,
MissionId = missionDefinition.Id,
Status = MissionStatus.Pending,
+ MissionRunPriority = MissionRunPriority.Normal,
DesiredStartTime = scheduledMissionQuery.DesiredStartTime,
Tasks = missionTasks,
InstallationCode = missionDefinition.InstallationCode,
@@ -228,6 +229,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery
Robot = robot,
MissionId = scheduledMissionDefinition.Id,
Status = MissionStatus.Pending,
+ MissionRunPriority = MissionRunPriority.Normal,
DesiredStartTime = scheduledMissionQuery.DesiredStartTime,
Tasks = missionTasks,
InstallationCode = scheduledMissionQuery.InstallationCode,
@@ -287,11 +289,11 @@ [FromBody] CustomMissionQuery customMissionQuery
var missionTasks = customMissionQuery.Tasks.Select(task => new MissionTask(task)).ToList();
MissionDefinition? customMissionDefinition;
- try { customMissionDefinition = await _missionSchedulingService.FindExistingOrCreateCustomMissionDefinition(customMissionQuery, missionTasks); }
+ try { customMissionDefinition = await _customMissionSchedulingService.FindExistingOrCreateCustomMissionDefinition(customMissionQuery, missionTasks); }
catch (SourceException e) { return StatusCode(StatusCodes.Status502BadGateway, e.Message); }
MissionRun? newMissionRun;
- try { newMissionRun = await _missionSchedulingService.QueueCustomMissionRun(customMissionQuery, customMissionDefinition.Id, robot.Id, missionTasks); }
+ try { newMissionRun = await _customMissionSchedulingService.QueueCustomMissionRun(customMissionQuery, customMissionDefinition.Id, robot.Id, missionTasks); }
catch (Exception e) when (e is RobotNotFoundException or MissionNotFoundException) { return NotFound(e.Message); }
return CreatedAtAction(nameof(Create), new
diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs
index 9fc8814f6..b94c2b747 100644
--- a/backend/api/Controllers/RobotController.cs
+++ b/backend/api/Controllers/RobotController.cs
@@ -6,905 +6,765 @@
using Api.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-
-namespace Api.Controllers;
-
-[ApiController]
-[Route("robots")]
-public class RobotController : ControllerBase
+namespace Api.Controllers
{
- private readonly ILogger _logger;
- private readonly IRobotService _robotService;
- private readonly IIsarService _isarService;
- private readonly IMissionRunService _missionRunService;
- private readonly IRobotModelService _robotModelService;
- private readonly IAreaService _areaService;
- private readonly IEchoService _echoService;
-
- public RobotController(
- ILogger logger,
- IRobotService robotService,
- IIsarService isarService,
- IMissionRunService missionRunService,
- IRobotModelService robotModelService,
- IAreaService areaService,
- IEchoService echoService
- )
- {
- _logger = logger;
- _robotService = robotService;
- _isarService = isarService;
- _missionRunService = missionRunService;
- _robotModelService = robotModelService;
- _areaService = areaService;
- _echoService = echoService;
- }
-
- ///
- /// List all robots on the installation.
- ///
- ///
- /// This query gets all robots
- ///
- [HttpGet]
- [Authorize(Roles = Role.Any)]
- [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task>> GetRobots()
- {
- try
- {
- var robots = await _robotService.ReadAll();
- return Ok(robots);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Error during GET of robots from database");
- throw;
- }
- }
-
- ///
- /// Gets the robot with the specified id
- ///
- ///
- /// This query gets the robot with the specified id
- ///
- [HttpGet]
- [Authorize(Roles = Role.Any)]
- [Route("{id}")]
- [ProducesResponseType(typeof(Robot), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task> GetRobotById([FromRoute] string id)
+ [ApiController]
+ [Route("robots")]
+ public class RobotController : ControllerBase
{
- _logger.LogInformation("Getting robot with id={id}", id);
- try
- {
- var robot = await _robotService.ReadById(id);
- if (robot == null)
+ private readonly IAreaService _areaService;
+ private readonly IIsarService _isarService;
+ private readonly ILogger _logger;
+ private readonly IMissionRunService _missionRunService;
+ private readonly IRobotModelService _robotModelService;
+ private readonly IRobotService _robotService;
+
+ public RobotController(
+ ILogger logger,
+ IRobotService robotService,
+ IIsarService isarService,
+ IMissionRunService missionRunService,
+ IRobotModelService robotModelService,
+ IAreaService areaService
+ )
+ {
+ _logger = logger;
+ _robotService = robotService;
+ _isarService = isarService;
+ _missionRunService = missionRunService;
+ _robotModelService = robotModelService;
+ _areaService = areaService;
+ }
+
+ ///
+ /// List all robots on the installation.
+ ///
+ ///
+ /// This query gets all robots
+ ///
+ [HttpGet]
+ [Authorize(Roles = Role.Any)]
+ [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetRobots()
+ {
+ try
{
- _logger.LogWarning("Could not find robot with id={id}", id);
- return NotFound();
+ var robots = await _robotService.ReadAll();
+ return Ok(robots);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error during GET of robots from database");
+ throw;
}
-
- _logger.LogInformation("Successful GET of robot with id={id}", id);
- return Ok(robot);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Error during GET of robot with id={id}", id);
- throw;
- }
- }
-
- ///
- /// Create robot and add to database
- ///
- ///
- /// This query creates a robot and adds it to the database
- ///
- [HttpPost]
- [Authorize(Roles = Role.Admin)]
- [ProducesResponseType(typeof(Robot), StatusCodes.Status201Created)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task> CreateRobot([FromBody] CreateRobotQuery robotQuery)
- {
- _logger.LogInformation("Creating new robot");
- try
- {
- var robotModel = await _robotModelService.ReadByRobotType(robotQuery.RobotType);
- if (robotModel == null)
- return BadRequest(
- $"No robot model exists with robot type '{robotQuery.RobotType}'"
- );
-
- var robot = new Robot(robotQuery) { Model = robotModel };
-
- var newRobot = await _robotService.Create(robot);
- _logger.LogInformation("Succesfully created new robot");
- return CreatedAtAction(nameof(GetRobotById), new { id = newRobot.Id }, newRobot);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Error while creating new robot");
- throw;
- }
- }
-
- ///
- /// Updates a robot in the database
- ///
- ///
- ///
- /// The robot was successfully updated
- /// The robot data is invalid
- /// There was no robot with the given ID in the database
- [HttpPut]
- [Authorize(Roles = Role.Admin)]
- [Route("{id}")]
- [ProducesResponseType(typeof(Robot), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task> UpdateRobot(
- [FromRoute] string id,
- [FromBody] Robot robot
- )
- {
- _logger.LogInformation("Updating robot with id={id}", id);
-
- if (!ModelState.IsValid)
- return BadRequest("Invalid data.");
-
- if (id != robot.Id)
- {
- _logger.LogWarning("Id: {id} not corresponding to updated robot", id);
- return BadRequest("Inconsistent Id");
- }
-
- try
- {
- var updatedRobot = await _robotService.Update(robot);
-
- _logger.LogInformation("Successful PUT of robot to database");
-
- return Ok(updatedRobot);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Error while updating robot with id={id}", id);
- throw;
- }
- }
-
- ///
- /// Deletes the robot with the specified id from the database.
- ///
- [HttpDelete]
- [Authorize(Roles = Role.Admin)]
- [Route("{id}")]
- [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task> DeleteRobot([FromRoute] string id)
- {
- var robot = await _robotService.Delete(id);
- if (robot is null)
- return NotFound($"Robot with id {id} not found");
- return Ok(robot);
- }
-
- ///
- /// Updates a robot's status in the database
- ///
- ///
- ///
- /// The robot status was succesfully updated
- /// The robot data is invalid
- /// There was no robot with the given ID in the database
- [HttpPut]
- [Authorize(Roles = Role.Admin)]
- [Route("{id}/status")]
- [ProducesResponseType(typeof(Robot), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task> UpdateRobotStatus(
- [FromRoute] string id,
- [FromBody] RobotStatus robotStatus
- )
- {
- _logger.LogInformation("Updating robot status with id={id}", id);
-
- if (!ModelState.IsValid)
- return BadRequest("Invalid data.");
-
- var robot = await _robotService.ReadById(id);
- if (robot == null)
- return NotFound($"No robot with id: {id} could be found");
-
- robot.Status = robotStatus;
- try
- {
- var updatedRobot = await _robotService.Update(robot);
-
- _logger.LogInformation("Successful PUT of robot to database");
-
- return Ok(updatedRobot);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Error while updating status for robot with id={id}", id);
- throw;
}
- }
- ///
- /// Get video streams for a given robot
- ///
- ///
- /// Retrieves the video streams available for the given robot
- ///
- [HttpGet]
- [Authorize(Roles = Role.User)]
- [Route("{robotId}/video-streams/")]
- [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task>> GetVideoStreams([FromRoute] string robotId)
- {
- var robot = await _robotService.ReadById(robotId);
- if (robot == null)
- {
- _logger.LogWarning("Could not find robot with id={id}", robotId);
- return NotFound();
+ ///
+ /// Gets the robot with the specified id
+ ///
+ ///
+ /// This query gets the robot with the specified id
+ ///
+ [HttpGet]
+ [Authorize(Roles = Role.Any)]
+ [Route("{id}")]
+ [ProducesResponseType(typeof(Robot), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> GetRobotById([FromRoute] string id)
+ {
+ _logger.LogInformation("Getting robot with id={id}", id);
+ try
+ {
+ var robot = await _robotService.ReadById(id);
+ if (robot == null)
+ {
+ _logger.LogWarning("Could not find robot with id={id}", id);
+ return NotFound();
+ }
+
+ _logger.LogInformation("Successful GET of robot with id={id}", id);
+ return Ok(robot);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error during GET of robot with id={id}", id);
+ throw;
+ }
}
- return Ok(robot.VideoStreams);
- }
-
- ///
- /// Add a video stream to a given robot
- ///
- ///
- /// Adds a provided video stream to the given robot
- ///
- [HttpPost]
- [Authorize(Roles = Role.Admin)]
- [Route("{robotId}/video-streams/")]
- [ProducesResponseType(typeof(Robot), StatusCodes.Status201Created)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task> CreateVideoStream(
- [FromRoute] string robotId,
- [FromBody] VideoStream videoStream
- )
- {
- var robot = await _robotService.ReadById(robotId);
- if (robot == null)
- {
- _logger.LogWarning("Could not find robot with id={id}", robotId);
- return NotFound();
+ ///
+ /// Create robot and add to database
+ ///
+ ///
+ /// This query creates a robot and adds it to the database
+ ///
+ [HttpPost]
+ [Authorize(Roles = Role.Admin)]
+ [ProducesResponseType(typeof(Robot), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> CreateRobot([FromBody] CreateRobotQuery robotQuery)
+ {
+ _logger.LogInformation("Creating new robot");
+ try
+ {
+ var robotModel = await _robotModelService.ReadByRobotType(robotQuery.RobotType);
+ if (robotModel == null)
+ {
+ return BadRequest(
+ $"No robot model exists with robot type '{robotQuery.RobotType}'"
+ );
+ }
+
+ var robot = new Robot(robotQuery)
+ {
+ Model = robotModel
+ };
+
+ var newRobot = await _robotService.Create(robot);
+ _logger.LogInformation("Succesfully created new robot");
+ return CreatedAtAction(nameof(GetRobotById), new
+ {
+ id = newRobot.Id
+ }, newRobot);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error while creating new robot");
+ throw;
+ }
}
- robot.VideoStreams.Add(videoStream);
-
- try
- {
- var updatedRobot = await _robotService.Update(robot);
-
- return CreatedAtAction(
- nameof(GetVideoStreams),
- new { robotId = updatedRobot.Id },
- updatedRobot
- );
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Error adding video stream to robot");
- throw;
- }
- }
+ ///
+ /// Updates a robot in the database
+ ///
+ ///
+ ///
+ /// The robot was successfully updated
+ /// The robot data is invalid
+ /// There was no robot with the given ID in the database
+ [HttpPut]
+ [Authorize(Roles = Role.Admin)]
+ [Route("{id}")]
+ [ProducesResponseType(typeof(Robot), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> UpdateRobot(
+ [FromRoute] string id,
+ [FromBody] Robot robot
+ )
+ {
+ _logger.LogInformation("Updating robot with id={id}", id);
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest("Invalid data.");
+ }
- ///
- /// Start the mission in the database with the corresponding 'missionRunId' for the robot with id 'robotId'
- ///
- ///
- /// This query starts a mission for a given robot
- ///
- [HttpPost]
- [Authorize(Roles = Role.Admin)]
- [Route("{robotId}/start/{missionRunId}")]
- [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task> StartMission(
- [FromRoute] string robotId,
- [FromRoute] string missionRunId
- )
- {
- var robot = await _robotService.ReadById(robotId);
- if (robot == null)
- {
- _logger.LogWarning("Could not find robot with id={id}", robotId);
- return NotFound("Robot not found");
- }
+ if (id != robot.Id)
+ {
+ _logger.LogWarning("Id: {id} not corresponding to updated robot", id);
+ return BadRequest("Inconsistent Id");
+ }
- if (robot.Status is not RobotStatus.Available)
- {
- _logger.LogWarning(
- "Robot '{id}' is not available ({status})",
- robotId,
- robot.Status.ToString()
- );
- return Conflict($"The Robot is not available ({robot.Status})");
- }
+ try
+ {
+ var updatedRobot = await _robotService.Update(robot);
- var missionRun = await _missionRunService.ReadById(missionRunId);
+ _logger.LogInformation("Successful PUT of robot to database");
- if (missionRun == null)
- {
- _logger.LogWarning("Could not find mission with id={id}", missionRunId);
- return NotFound("Mission not found");
+ return Ok(updatedRobot);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error while updating robot with id={id}", id);
+ throw;
+ }
}
- IsarMission isarMission;
- try
- {
- isarMission = await _isarService.StartMission(robot, missionRun);
- }
- catch (HttpRequestException e)
- {
- string message = $"Could not reach ISAR at {robot.IsarUri}";
- _logger.LogError(e, "{message}", message);
- OnIsarUnavailable(robot);
- return StatusCode(StatusCodes.Status502BadGateway, message);
- }
- catch (MissionException e)
- {
- _logger.LogError(e, "Error while starting ISAR mission");
- return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
- }
- catch (JsonException e)
- {
- string message = "Error while processing of the response from ISAR";
- _logger.LogError(e, "{message}", message);
- return StatusCode(StatusCodes.Status500InternalServerError, message);
- }
- catch (RobotPositionNotFoundException e)
- {
- string message =
- "A suitable robot position could not be found for one or more of the desired tags";
- _logger.LogError(e, "{message}", message);
- return StatusCode(StatusCodes.Status500InternalServerError, message);
+ ///
+ /// Deletes the robot with the specified id from the database.
+ ///
+ [HttpDelete]
+ [Authorize(Roles = Role.Admin)]
+ [Route("{id}")]
+ [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> DeleteRobot([FromRoute] string id)
+ {
+ var robot = await _robotService.Delete(id);
+ if (robot is null)
+ {
+ return NotFound($"Robot with id {id} not found");
+ }
+ return Ok(robot);
}
- missionRun.UpdateWithIsarInfo(isarMission);
- missionRun.Status = MissionStatus.Ongoing;
-
- await _missionRunService.Update(missionRun);
+ ///
+ /// Updates a robot's status in the database
+ ///
+ ///
+ ///
+ /// The robot status was succesfully updated
+ /// The robot data is invalid
+ /// There was no robot with the given ID in the database
+ [HttpPut]
+ [Authorize(Roles = Role.Admin)]
+ [Route("{id}/status")]
+ [ProducesResponseType(typeof(Robot), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> UpdateRobotStatus(
+ [FromRoute] string id,
+ [FromBody] RobotStatus robotStatus
+ )
+ {
+ _logger.LogInformation("Updating robot status with id={id}", id);
+
+ if (!ModelState.IsValid)
+ {
+ return BadRequest("Invalid data.");
+ }
- if (robot.CurrentMissionId != null)
- {
- var orphanedMissionRun = await _missionRunService.ReadById(robot.CurrentMissionId);
- if (orphanedMissionRun != null)
+ var robot = await _robotService.ReadById(id);
+ if (robot == null)
{
- orphanedMissionRun.SetToFailed();
- await _missionRunService.Update(orphanedMissionRun);
+ return NotFound($"No robot with id: {id} could be found");
}
- }
- robot.Status = RobotStatus.Busy;
- robot.CurrentMissionId = missionRun.Id;
- await _robotService.Update(robot);
+ robot.Status = robotStatus;
+ try
+ {
+ var updatedRobot = await _robotService.Update(robot);
- return Ok(missionRun);
- }
+ _logger.LogInformation("Successful PUT of robot to database");
- ///
- /// Stops the current mission on a robot
- ///
- ///
- /// This query stops the current mission for a given robot
- ///
- [HttpPost]
- [Authorize(Roles = Role.User)]
- [Route("{robotId}/stop/")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task StopMission([FromRoute] string robotId)
- {
- var robot = await _robotService.ReadById(robotId);
- if (robot == null)
- {
- _logger.LogWarning("Could not find robot with id={id}", robotId);
- return NotFound();
+ return Ok(updatedRobot);
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Error while updating status for robot with id={id}", id);
+ throw;
+ }
}
- try
- {
- await _isarService.StopMission(robot);
- robot.CurrentMissionId = null;
- await _robotService.Update(robot);
- }
- catch (HttpRequestException e)
- {
- string message = "Error connecting to ISAR while stopping mission";
- _logger.LogError(e, "{message}", message);
- OnIsarUnavailable(robot);
- return StatusCode(StatusCodes.Status502BadGateway, message);
- }
- catch (MissionException e)
- {
- _logger.LogError(e, "Error while stopping ISAR mission");
- return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
- }
- catch (JsonException e)
- {
- string message = "Error while processing the response from ISAR";
- _logger.LogError(e, "{message}", message);
- return StatusCode(StatusCodes.Status500InternalServerError, message);
- }
+ ///
+ /// Get video streams for a given robot
+ ///
+ ///
+ /// Retrieves the video streams available for the given robot
+ ///
+ [HttpGet]
+ [Authorize(Roles = Role.User)]
+ [Route("{robotId}/video-streams/")]
+ [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetVideoStreams([FromRoute] string robotId)
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogWarning("Could not find robot with id={id}", robotId);
+ return NotFound();
+ }
- return NoContent();
- }
+ return Ok(robot.VideoStreams);
+ }
+
+ ///
+ /// Add a video stream to a given robot
+ ///
+ ///
+ /// Adds a provided video stream to the given robot
+ ///
+ [HttpPost]
+ [Authorize(Roles = Role.Admin)]
+ [Route("{robotId}/video-streams/")]
+ [ProducesResponseType(typeof(Robot), StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> CreateVideoStream(
+ [FromRoute] string robotId,
+ [FromBody] VideoStream videoStream
+ )
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogWarning("Could not find robot with id={id}", robotId);
+ return NotFound();
+ }
- ///
- /// Pause the current mission on a robot
- ///
- ///
- /// This query pauses the current mission for a robot
- ///
- [HttpPost]
- [Authorize(Roles = Role.User)]
- [Route("{robotId}/pause/")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task PauseMission([FromRoute] string robotId)
- {
- var robot = await _robotService.ReadById(robotId);
- if (robot == null)
- {
- _logger.LogWarning("Could not find robot with id={id}", robotId);
- return NotFound();
- }
+ robot.VideoStreams.Add(videoStream);
- try
- {
- await _isarService.PauseMission(robot);
- }
- catch (HttpRequestException e)
- {
- string message = "Error connecting to ISAR while pausing mission";
- _logger.LogError(e, "{message}", message);
- OnIsarUnavailable(robot);
- return StatusCode(StatusCodes.Status502BadGateway, message);
- }
- catch (MissionException e)
- {
- _logger.LogError(e, "Error while pausing ISAR mission");
- return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
- }
- catch (JsonException e)
- {
- string message = "Error while processing of the response from ISAR";
- _logger.LogError(e, "{message}", message);
- return StatusCode(StatusCodes.Status500InternalServerError, message);
+ try
+ {
+ var updatedRobot = await _robotService.Update(robot);
+
+ return CreatedAtAction(
+ nameof(GetVideoStreams),
+ new
+ {
+ robotId = updatedRobot.Id
+ },
+ updatedRobot
+ );
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error adding video stream to robot");
+ throw;
+ }
}
- return NoContent();
- }
+ ///
+ /// Start the mission in the database with the corresponding 'missionRunId' for the robot with id 'robotId'
+ ///
+ ///
+ /// This query starts a mission for a given robot
+ ///
+ [HttpPost]
+ [Authorize(Roles = Role.Admin)]
+ [Route("{robotId}/start/{missionRunId}")]
+ [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> StartMission(
+ [FromRoute] string robotId,
+ [FromRoute] string missionRunId
+ )
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogWarning("Could not find robot with id={id}", robotId);
+ return NotFound("Robot not found");
+ }
- ///
- /// Resume paused mission on a robot
- ///
- ///
- /// This query resumes the currently paused mission for a robot
- ///
- [HttpPost]
- [Authorize(Roles = Role.User)]
- [Route("{robotId}/resume/")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task ResumeMission([FromRoute] string robotId)
- {
- var robot = await _robotService.ReadById(robotId);
- if (robot == null)
- {
- _logger.LogWarning("Could not find robot with id={id}", robotId);
- return NotFound();
- }
+ if (robot.Status is not RobotStatus.Available)
+ {
+ _logger.LogWarning(
+ "Robot '{id}' is not available ({status})",
+ robotId,
+ robot.Status.ToString()
+ );
+ return Conflict($"The Robot is not available ({robot.Status})");
+ }
- try
- {
- await _isarService.ResumeMission(robot);
- }
- catch (HttpRequestException e)
- {
- string message = "Error connecting to ISAR while resuming mission";
- _logger.LogError(e, "{message}", message);
- OnIsarUnavailable(robot);
- return StatusCode(StatusCodes.Status502BadGateway, message);
- }
- catch (MissionException e)
- {
- _logger.LogError(e, "Error while resuming ISAR mission");
- return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
- }
- catch (JsonException e)
- {
- string message = "Error while processing of the response from ISAR";
- _logger.LogError(e, "{message}", message);
- return StatusCode(StatusCodes.Status500InternalServerError, message);
- }
+ var missionRun = await _missionRunService.ReadById(missionRunId);
- return NoContent();
- }
+ if (missionRun == null)
+ {
+ _logger.LogWarning("Could not find mission with id={id}", missionRunId);
+ return NotFound("Mission not found");
+ }
+ IsarMission isarMission;
+ try
+ {
+ isarMission = await _isarService.StartMission(robot, missionRun);
+ }
+ catch (HttpRequestException e)
+ {
+ string message = $"Could not reach ISAR at {robot.IsarUri}";
+ _logger.LogError(e, "{message}", message);
+ OnIsarUnavailable(robot);
+ return StatusCode(StatusCodes.Status502BadGateway, message);
+ }
+ catch (MissionException e)
+ {
+ _logger.LogError(e, "Error while starting ISAR mission");
+ return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
+ }
+ catch (JsonException e)
+ {
+ string message = "Error while processing of the response from ISAR";
+ _logger.LogError(e, "{message}", message);
+ return StatusCode(StatusCodes.Status500InternalServerError, message);
+ }
+ catch (RobotPositionNotFoundException e)
+ {
+ string message =
+ "A suitable robot position could not be found for one or more of the desired tags";
+ _logger.LogError(e, "{message}", message);
+ return StatusCode(StatusCodes.Status500InternalServerError, message);
+ }
- ///
- /// Post new arm position ("battery_change", "transport", "lookout") for the robot with id 'robotId'
- ///
- ///
- /// This query moves the arm to a given position for a given robot
- ///
- [HttpPut]
- [Authorize(Roles = Role.User)]
- [Route("{robotId}/SetArmPosition/{armPosition}")]
- [ProducesResponseType(StatusCodes.Status204NoContent)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task SetArmPosition(
- [FromRoute] string robotId,
- [FromRoute] string armPosition
- )
- {
- var robot = await _robotService.ReadById(robotId);
- if (robot == null)
- {
- string errorMessage = $"Could not find robot with id {robotId}";
- _logger.LogWarning(errorMessage);
- return NotFound(errorMessage);
- }
+ missionRun.UpdateWithIsarInfo(isarMission);
+ missionRun.Status = MissionStatus.Ongoing;
- if (robot.Status is not RobotStatus.Available)
- {
- string errorMessage = $"Robot {robotId} has status ({robot.Status}) and is not available";
- _logger.LogWarning(errorMessage);
- return Conflict(errorMessage);
- }
- try
- {
- await _isarService.StartMoveArm(robot, armPosition);
- }
- catch (HttpRequestException e)
- {
- string errorMessage = $"Error connecting to ISAR at {robot.IsarUri}";
- _logger.LogError(e, "{Message}", errorMessage);
- OnIsarUnavailable(robot);
- return StatusCode(StatusCodes.Status502BadGateway, errorMessage);
- }
- catch (MissionException e)
- {
- string errorMessage = $"An error occurred while setting the arm position mission";
- _logger.LogError(e, "{Message}", errorMessage);
- return StatusCode(StatusCodes.Status502BadGateway, errorMessage);
- }
- catch (JsonException e)
- {
- string errorMessage = "Error while processing of the response from ISAR";
- _logger.LogError(e, "{Message}", errorMessage);
- return StatusCode(StatusCodes.Status500InternalServerError, errorMessage);
- }
+ await _missionRunService.Update(missionRun);
- return NoContent();
- }
+ if (robot.CurrentMissionId != null)
+ {
+ var orphanedMissionRun = await _missionRunService.ReadById(robot.CurrentMissionId);
+ if (orphanedMissionRun != null)
+ {
+ orphanedMissionRun.SetToFailed();
+ await _missionRunService.Update(orphanedMissionRun);
+ }
+ }
- ///
- /// Start a localization mission with localization in the pose 'localizationPose' for the robot with id 'robotId'
- ///
- ///
- /// This query starts a localization for a given robot
- ///
- [HttpPost]
- [Authorize(Roles = Role.User)]
- [Route("start-localization")]
- [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task> StartLocalizationMission(
- [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery
- )
- {
- var robot = await _robotService.ReadById(scheduleLocalizationMissionQuery.RobotId);
- if (robot == null)
- {
- _logger.LogWarning("Could not find robot with id={id}", scheduleLocalizationMissionQuery.RobotId);
- return NotFound("Robot not found");
- }
+ robot.Status = RobotStatus.Busy;
+ robot.CurrentMissionId = missionRun.Id;
+ await _robotService.Update(robot);
- if (robot.Status is not RobotStatus.Available)
- {
- _logger.LogWarning(
- "Robot '{id}' is not available ({status})",
- scheduleLocalizationMissionQuery.RobotId,
- robot.Status.ToString()
- );
- return Conflict($"The Robot is not available ({robot.Status})");
- }
+ return Ok(missionRun);
+ }
+
+ ///
+ /// Stops the current mission on a robot
+ ///
+ ///
+ /// This query stops the current mission for a given robot
+ ///
+ [HttpPost]
+ [Authorize(Roles = Role.User)]
+ [Route("{robotId}/stop/")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task StopMission([FromRoute] string robotId)
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogWarning("Could not find robot with id={id}", robotId);
+ return NotFound();
+ }
- var area = await _areaService.ReadById(scheduleLocalizationMissionQuery.AreaId);
+ try
+ {
+ await _isarService.StopMission(robot);
+ robot.CurrentMissionId = null;
+ await _robotService.Update(robot);
+ }
+ catch (HttpRequestException e)
+ {
+ string message = "Error connecting to ISAR while stopping mission";
+ _logger.LogError(e, "{message}", message);
+ OnIsarUnavailable(robot);
+ return StatusCode(StatusCodes.Status502BadGateway, message);
+ }
+ catch (MissionException e)
+ {
+ _logger.LogError(e, "Error while stopping ISAR mission");
+ return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
+ }
+ catch (JsonException e)
+ {
+ string message = "Error while processing the response from ISAR";
+ _logger.LogError(e, "{message}", message);
+ return StatusCode(StatusCodes.Status500InternalServerError, message);
+ }
- if (area == null)
- {
- _logger.LogWarning("Could not find area with id={id}", scheduleLocalizationMissionQuery.AreaId);
- return NotFound("Area not found");
- }
+ return NoContent();
+ }
+
+ ///
+ /// Pause the current mission on a robot
+ ///
+ ///
+ /// This query pauses the current mission for a robot
+ ///
+ [HttpPost]
+ [Authorize(Roles = Role.User)]
+ [Route("{robotId}/pause/")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task PauseMission([FromRoute] string robotId)
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogWarning("Could not find robot with id={id}", robotId);
+ return NotFound();
+ }
- var missionRun = new MissionRun
- {
- Name = "Localization Mission",
- Robot = robot,
- InstallationCode = "NA",
- Area = area,
- Status = MissionStatus.Pending,
- DesiredStartTime = DateTimeOffset.UtcNow,
- Tasks = new List(),
- Map = new MapMetadata()
- };
-
- IsarMission isarMission;
- try
- {
- isarMission = await _isarService.StartLocalizationMission(robot, scheduleLocalizationMissionQuery.LocalizationPose);
- }
- catch (HttpRequestException e)
- {
- string message = $"Could not reach ISAR at {robot.IsarUri}";
- _logger.LogError(e, "{Message}", message);
- OnIsarUnavailable(robot);
- return StatusCode(StatusCodes.Status502BadGateway, message);
- }
- catch (MissionException e)
- {
- _logger.LogError(e, "Error while starting ISAR localization mission");
- return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
- }
- catch (JsonException e)
- {
- string message = "Error while processing of the response from ISAR";
- _logger.LogError(e, "{Message}", message);
- return StatusCode(StatusCodes.Status500InternalServerError, message);
- }
+ try
+ {
+ await _isarService.PauseMission(robot);
+ }
+ catch (HttpRequestException e)
+ {
+ string message = "Error connecting to ISAR while pausing mission";
+ _logger.LogError(e, "{message}", message);
+ OnIsarUnavailable(robot);
+ return StatusCode(StatusCodes.Status502BadGateway, message);
+ }
+ catch (MissionException e)
+ {
+ _logger.LogError(e, "Error while pausing ISAR mission");
+ return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
+ }
+ catch (JsonException e)
+ {
+ string message = "Error while processing of the response from ISAR";
+ _logger.LogError(e, "{message}", message);
+ return StatusCode(StatusCodes.Status500InternalServerError, message);
+ }
- missionRun.UpdateWithIsarInfo(isarMission);
- missionRun.Status = MissionStatus.Ongoing;
+ return NoContent();
+ }
+
+ ///
+ /// Resume paused mission on a robot
+ ///
+ ///
+ /// This query resumes the currently paused mission for a robot
+ ///
+ [HttpPost]
+ [Authorize(Roles = Role.User)]
+ [Route("{robotId}/resume/")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task ResumeMission([FromRoute] string robotId)
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogWarning("Could not find robot with id={id}", robotId);
+ return NotFound();
+ }
- await _missionRunService.Create(missionRun);
+ try
+ {
+ await _isarService.ResumeMission(robot);
+ }
+ catch (HttpRequestException e)
+ {
+ string message = "Error connecting to ISAR while resuming mission";
+ _logger.LogError(e, "{message}", message);
+ OnIsarUnavailable(robot);
+ return StatusCode(StatusCodes.Status502BadGateway, message);
+ }
+ catch (MissionException e)
+ {
+ _logger.LogError(e, "Error while resuming ISAR mission");
+ return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
+ }
+ catch (JsonException e)
+ {
+ string message = "Error while processing of the response from ISAR";
+ _logger.LogError(e, "{message}", message);
+ return StatusCode(StatusCodes.Status500InternalServerError, message);
+ }
- robot.Status = RobotStatus.Busy;
- robot.CurrentMissionId = missionRun.Id;
- await _robotService.Update(robot);
- robot.CurrentArea = area;
- return Ok(missionRun);
- }
+ return NoContent();
+ }
+
+
+ ///
+ /// Post new arm position ("battery_change", "transport", "lookout") for the robot with id 'robotId'
+ ///
+ ///
+ /// This query moves the arm to a given position for a given robot
+ ///
+ [HttpPut]
+ [Authorize(Roles = Role.User)]
+ [Route("{robotId}/SetArmPosition/{armPosition}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task SetArmPosition(
+ [FromRoute] string robotId,
+ [FromRoute] string armPosition
+ )
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ string errorMessage = $"Could not find robot with id {robotId}";
+ _logger.LogWarning(errorMessage);
+ return NotFound(errorMessage);
+ }
- ///
- /// Starts a mission which drives the robot to the nearest safe position
- ///
- ///
- /// This query starts a localization for a given robot
- ///
- [HttpPost]
- [Route("{robotId}/{installation}/{areaName}/go-to-safe-position")]
- [Authorize(Roles = Role.User)]
- [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)]
- [ProducesResponseType(StatusCodes.Status400BadRequest)]
- [ProducesResponseType(StatusCodes.Status401Unauthorized)]
- [ProducesResponseType(StatusCodes.Status403Forbidden)]
- [ProducesResponseType(StatusCodes.Status404NotFound)]
- [ProducesResponseType(StatusCodes.Status500InternalServerError)]
- public async Task> SendRobotToSafePosition(
- [FromRoute] string robotId,
- [FromRoute] string installation,
- [FromRoute] string areaName
- )
- {
- var robot = await _robotService.ReadById(robotId);
- if (robot == null)
- {
- _logger.LogWarning("Could not find robot with id={id}", robotId);
- return NotFound("Robot not found");
- }
+ if (robot.Status is not RobotStatus.Available)
+ {
+ string errorMessage = $"Robot {robotId} has status ({robot.Status}) and is not available";
+ _logger.LogWarning(errorMessage);
+ return Conflict(errorMessage);
+ }
+ try
+ {
+ await _isarService.StartMoveArm(robot, armPosition);
+ }
+ catch (HttpRequestException e)
+ {
+ string errorMessage = $"Error connecting to ISAR at {robot.IsarUri}";
+ _logger.LogError(e, "{Message}", errorMessage);
+ OnIsarUnavailable(robot);
+ return StatusCode(StatusCodes.Status502BadGateway, errorMessage);
+ }
+ catch (MissionException e)
+ {
+ string errorMessage = "An error occurred while setting the arm position mission";
+ _logger.LogError(e, "{Message}", errorMessage);
+ return StatusCode(StatusCodes.Status502BadGateway, errorMessage);
+ }
+ catch (JsonException e)
+ {
+ string errorMessage = "Error while processing of the response from ISAR";
+ _logger.LogError(e, "{Message}", errorMessage);
+ return StatusCode(StatusCodes.Status500InternalServerError, errorMessage);
+ }
- var installations = await _areaService.ReadByInstallation(installation);
+ return NoContent();
+ }
+
+ ///
+ /// Start a localization mission with localization in the pose 'localizationPose' for the robot with id 'robotId'
+ ///
+ ///
+ /// This query starts a localization for a given robot
+ ///
+ [HttpPost]
+ [Authorize(Roles = Role.User)]
+ [Route("start-localization")]
+ [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> StartLocalizationMission(
+ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery
+ )
+ {
+ var robot = await _robotService.ReadById(scheduleLocalizationMissionQuery.RobotId);
+ if (robot == null)
+ {
+ _logger.LogWarning("Could not find robot with id={id}", scheduleLocalizationMissionQuery.RobotId);
+ return NotFound("Robot not found");
+ }
- if (!installations.Any())
- {
- _logger.LogWarning("Could not find installation={installation}", installation);
- return NotFound("No installation found");
- }
+ if (robot.Status is not RobotStatus.Available)
+ {
+ _logger.LogWarning(
+ "Robot '{id}' is not available ({status})",
+ scheduleLocalizationMissionQuery.RobotId,
+ robot.Status.ToString()
+ );
+ return Conflict($"The Robot is not available ({robot.Status})");
+ }
- var area = await _areaService.ReadByInstallationAndName(installation, areaName);
- if (area is null)
- {
- _logger.LogWarning("Could not find area={areaName}", areaName);
- return NotFound("No area found");
- }
+ var area = await _areaService.ReadById(scheduleLocalizationMissionQuery.AreaId);
- if (area.SafePositions.Count < 1)
- {
- _logger.LogWarning("No safe position for installation={installation}, area={areaName}", installation, areaName);
- return NotFound("No safe positions found");
- }
+ if (area == null)
+ {
+ _logger.LogWarning("Could not find area with id={id}", scheduleLocalizationMissionQuery.AreaId);
+ return NotFound("Area not found");
+ }
- try
- {
- await _isarService.StopMission(robot);
- }
- catch (MissionException e)
- {
- // We want to continue driving to a safe position if the isar state is idle
- if (e.IsarStatusCode != 409)
+ var missionRun = new MissionRun
{
- _logger.LogError(e, "Error while stopping ISAR mission");
+ Name = "Localization Mission",
+ Robot = robot,
+ MissionRunPriority = MissionRunPriority.Normal,
+ InstallationCode = "NA",
+ Area = area,
+ Status = MissionStatus.Pending,
+ DesiredStartTime = DateTimeOffset.UtcNow,
+ Tasks = new List(),
+ Map = new MapMetadata()
+ };
+
+ IsarMission isarMission;
+ try
+ {
+ isarMission = await _isarService.StartLocalizationMission(robot, scheduleLocalizationMissionQuery.LocalizationPose);
+ }
+ catch (HttpRequestException e)
+ {
+ string message = $"Could not reach ISAR at {robot.IsarUri}";
+ _logger.LogError(e, "{Message}", message);
+ OnIsarUnavailable(robot);
+ return StatusCode(StatusCodes.Status502BadGateway, message);
+ }
+ catch (MissionException e)
+ {
+ _logger.LogError(e, "Error while starting ISAR localization mission");
return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
}
- }
- catch (Exception e)
- {
- string message = "Error in ISAR while stopping current mission, cannot drive to safe position";
- _logger.LogError(e, "{message}", message);
- OnIsarUnavailable(robot);
- return StatusCode(StatusCodes.Status502BadGateway, message);
- }
-
- var closestSafePosition = ClosestSafePosition(robot.Pose, area.SafePositions);
- // Cloning to avoid tracking same object
- var clonedPose = ObjectCopier.Clone(closestSafePosition);
- var customTaskQuery = new CustomTaskQuery
- {
- RobotPose = clonedPose,
- Inspections = new List(),
- InspectionTarget = new Position(),
- TaskOrder = 0
- };
- // TODO: The MissionId is nullable because of this mission
- var missionRun = new MissionRun
- {
- Name = "Drive to Safe Position",
- Robot = robot,
- InstallationCode = installation,
- Area = area,
- Status = MissionStatus.Pending,
- DesiredStartTime = DateTimeOffset.UtcNow,
- Tasks = new List(new[] { new MissionTask(customTaskQuery) }),
- Map = new MapMetadata()
- };
-
- IsarMission isarMission;
- try
- {
- isarMission = await _isarService.StartMission(robot, missionRun);
- }
- catch (HttpRequestException e)
- {
- string message = $"Could not reach ISAR at {robot.IsarUri}";
- _logger.LogError(e, "{message}", message);
- OnIsarUnavailable(robot);
- return StatusCode(StatusCodes.Status502BadGateway, message);
- }
- catch (MissionException e)
- {
- _logger.LogError(e, "Error while starting ISAR mission");
- return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}");
- }
- catch (JsonException e)
- {
- string message = "Error while processing of the response from ISAR";
- _logger.LogError(e, "{message}", message);
- return StatusCode(StatusCodes.Status500InternalServerError, message);
- }
-
- missionRun.UpdateWithIsarInfo(isarMission);
- missionRun.Status = MissionStatus.Ongoing;
-
- await _missionRunService.Create(missionRun);
-
- robot.Status = RobotStatus.Busy;
- robot.CurrentMissionId = missionRun.Id;
- await _robotService.Update(robot);
- return Ok(missionRun);
- }
-
- private async void OnIsarUnavailable(Robot robot)
- {
- robot.Enabled = false;
- robot.Status = RobotStatus.Offline;
- if (robot.CurrentMissionId != null)
- {
- var missionRun = await _missionRunService.ReadById(robot.CurrentMissionId);
- if (missionRun != null)
+ catch (JsonException e)
{
- missionRun.SetToFailed();
- await _missionRunService.Update(missionRun);
- _logger.LogWarning(
- "Mission '{id}' failed because ISAR could not be reached",
- missionRun.Id
- );
+ string message = "Error while processing of the response from ISAR";
+ _logger.LogError(e, "{Message}", message);
+ return StatusCode(StatusCodes.Status500InternalServerError, message);
}
- }
- robot.CurrentMissionId = null;
- await _robotService.Update(robot);
- }
- private static Pose ClosestSafePosition(Pose robotPose, IList safePositions)
- {
- if (safePositions == null || !safePositions.Any())
- {
- throw new ArgumentException("List of safe positions cannot be null or empty.");
- }
+ missionRun.UpdateWithIsarInfo(isarMission);
+ missionRun.Status = MissionStatus.Ongoing;
+
+ await _missionRunService.Create(missionRun);
- var closestPose = safePositions[0].Pose;
- float minDistance = CalculateDistance(robotPose, closestPose);
+ robot.Status = RobotStatus.Busy;
+ robot.CurrentMissionId = missionRun.Id;
+ robot.CurrentArea = area;
+ await _robotService.Update(robot);
+ return Ok(missionRun);
+ }
- for (int i = 1; i < safePositions.Count; i++)
+ private async void OnIsarUnavailable(Robot robot)
{
- float currentDistance = CalculateDistance(robotPose, safePositions[i].Pose);
- if (currentDistance < minDistance)
+ robot.Enabled = false;
+ robot.Status = RobotStatus.Offline;
+ if (robot.CurrentMissionId != null)
{
- minDistance = currentDistance;
- closestPose = safePositions[i].Pose;
+ var missionRun = await _missionRunService.ReadById(robot.CurrentMissionId);
+ if (missionRun != null)
+ {
+ missionRun.SetToFailed();
+ await _missionRunService.Update(missionRun);
+ _logger.LogWarning(
+ "Mission '{id}' failed because ISAR could not be reached",
+ missionRun.Id
+ );
+ }
}
+ robot.CurrentMissionId = null;
+ await _robotService.Update(robot);
}
- return closestPose;
- }
-
- private static float CalculateDistance(Pose pose1, Pose pose2)
- {
- var pos1 = pose1.Position;
- var pos2 = pose2.Position;
- return (float)Math.Sqrt(Math.Pow(pos1.X - pos2.X, 2) + Math.Pow(pos1.Y - pos2.Y, 2) + Math.Pow(pos1.Z - pos2.Z, 2));
}
}
diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs
index 8a05e325c..0aa50ae2a 100644
--- a/backend/api/Database/Context/InitDb.cs
+++ b/backend/api/Database/Context/InitDb.cs
@@ -68,7 +68,7 @@ private static List GetDecks()
Id = Guid.NewGuid().ToString(),
Plant = plants[0],
Installation = plants[0].Installation,
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
Name = "TestDeck"
};
@@ -77,7 +77,7 @@ private static List GetDecks()
Id = Guid.NewGuid().ToString(),
Plant = plants[0],
Installation = plants[0].Installation,
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
Name = "TestDeck2"
};
@@ -86,7 +86,7 @@ private static List GetDecks()
Id = Guid.NewGuid().ToString(),
Plant = plants[0],
Installation = plants[0].Installation,
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
Name = "TestDeck3"
};
@@ -95,7 +95,7 @@ private static List GetDecks()
Id = Guid.NewGuid().ToString(),
Plant = plants[0],
Installation = plants[0].Installation,
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
Name = "TestDeck4"
};
@@ -112,7 +112,7 @@ private static List GetAreas()
Installation = decks[0].Plant!.Installation,
Name = "AP320",
MapMetadata = new MapMetadata(),
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
SafePositions = new List()
};
@@ -124,7 +124,7 @@ private static List GetAreas()
Installation = decks[0].Plant!.Installation,
Name = "AP330",
MapMetadata = new MapMetadata(),
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
SafePositions = new List()
};
@@ -136,8 +136,11 @@ private static List GetAreas()
Installation = decks[0].Plant!.Installation,
Name = "testArea",
MapMetadata = new MapMetadata(),
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
SafePositions = new List()
+ {
+ new()
+ }
};
var area4 = new Area
@@ -148,7 +151,7 @@ private static List GetAreas()
Installation = decks[1].Plant.Installation,
Name = "testArea2",
MapMetadata = new MapMetadata(),
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
SafePositions = new List()
};
@@ -160,7 +163,7 @@ private static List GetAreas()
Installation = decks[2].Plant.Installation,
Name = "testArea3",
MapMetadata = new MapMetadata(),
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
SafePositions = new List()
};
@@ -172,7 +175,7 @@ private static List GetAreas()
Installation = decks[3].Plant.Installation,
Name = "testArea4",
MapMetadata = new MapMetadata(),
- DefaultLocalizationPose = null,
+ DefaultLocalizationPose = new DefaultLocalizationPose(),
SafePositions = new List()
};
diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs
index 15a9724e8..e89c75920 100644
--- a/backend/api/Database/Models/MissionRun.cs
+++ b/backend/api/Database/Models/MissionRun.cs
@@ -1,12 +1,15 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Api.Services.Models;
-
#pragma warning disable CS8618
namespace Api.Database.Models
{
public class MissionRun : SortableRecord
{
+
+ private MissionStatus _status;
+
+ private IList _tasks;
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
@@ -14,22 +17,22 @@ public class MissionRun : SortableRecord
//[Required] // See "Drive to Safe Position" mission in RobotController.cs
public string? MissionId { get; set; }
- [Required]
- [MaxLength(200)]
- public string Name { get; set; }
-
[Required]
public MissionStatus Status
{
- get { return _status; }
+ get => _status;
set
{
_status = value;
if (IsCompleted && EndTime is null)
+ {
EndTime = DateTimeOffset.UtcNow;
+ }
if (_status is MissionStatus.Ongoing && StartTime is null)
+ {
StartTime = DateTimeOffset.UtcNow;
+ }
}
}
@@ -47,10 +50,13 @@ public MissionStatus Status
[Required]
public IList Tasks
{
- get { return _tasks.OrderBy(t => t.TaskOrder).ToList(); }
- set { _tasks = value; }
+ get => _tasks.OrderBy(t => t.TaskOrder).ToList();
+ set => _tasks = value;
}
+ [Required]
+ public MissionRunPriority MissionRunPriority { get; set; }
+
[MaxLength(200)]
public string? IsarMissionId { get; set; }
@@ -65,15 +71,13 @@ public IList Tasks
public Area? Area { get; set; }
- private MissionStatus _status;
-
public bool IsCompleted =>
_status
is MissionStatus.Aborted
- or MissionStatus.Cancelled
- or MissionStatus.Successful
- or MissionStatus.PartiallySuccessful
- or MissionStatus.Failed;
+ or MissionStatus.Cancelled
+ or MissionStatus.Successful
+ or MissionStatus.PartiallySuccessful
+ or MissionStatus.Failed;
public MapMetadata? Map { get; set; }
@@ -82,11 +86,13 @@ or MissionStatus.PartiallySuccessful
public DateTimeOffset? EndTime { get; private set; }
///
- /// The estimated duration of the mission in seconds
+ /// The estimated duration of the mission in seconds
///
public uint? EstimatedDuration { get; set; }
- private IList _tasks;
+ [Required]
+ [MaxLength(200)]
+ public string Name { get; set; }
public void UpdateWithIsarInfo(IsarMission isarMission)
{
@@ -98,7 +104,6 @@ public void UpdateWithIsarInfo(IsarMission isarMission)
}
}
-#nullable enable
public MissionTask? GetTaskByIsarId(string isarTaskId)
{
return Tasks.FirstOrDefault(
@@ -108,8 +113,6 @@ public void UpdateWithIsarInfo(IsarMission isarMission)
);
}
-#nullable disable
-
public static MissionStatus MissionStatusFromString(string status)
{
return status switch
@@ -122,9 +125,9 @@ public static MissionStatus MissionStatusFromString(string status)
"paused" => MissionStatus.Paused,
"partially_successful" => MissionStatus.PartiallySuccessful,
_
- => throw new ArgumentException(
- $"Failed to parse mission status '{status}' as it's not supported"
- )
+ => throw new ArgumentException(
+ $"Failed to parse mission status '{status}' as it's not supported"
+ )
};
}
@@ -198,4 +201,11 @@ public enum MissionStatus
Successful,
PartiallySuccessful
}
+
+ public enum MissionRunPriority
+ {
+ Normal,
+ Response,
+ Emergency
+ }
}
diff --git a/backend/api/Database/Models/Robot.cs b/backend/api/Database/Models/Robot.cs
index ae9a8263c..53c9a300b 100644
--- a/backend/api/Database/Models/Robot.cs
+++ b/backend/api/Database/Models/Robot.cs
@@ -1,12 +1,52 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Api.Controllers.Models;
-
#pragma warning disable CS8618
namespace Api.Database.Models
{
public class Robot
{
+
+ public Robot()
+ {
+ VideoStreams = new List();
+ IsarId = "defaultIsarId";
+ Name = "defaultId";
+ SerialNumber = "defaultSerialNumber";
+ CurrentInstallation = "defaultAsset";
+ Status = RobotStatus.Offline;
+ Enabled = false;
+ Host = "localhost";
+ Port = 3000;
+ Pose = new Pose();
+ }
+
+ public Robot(CreateRobotQuery createQuery)
+ {
+ var videoStreams = new List();
+ foreach (var videoStreamQuery in createQuery.VideoStreams)
+ {
+ var videoStream = new VideoStream
+ {
+ Name = videoStreamQuery.Name,
+ Url = videoStreamQuery.Url,
+ Type = videoStreamQuery.Type
+ };
+ videoStreams.Add(videoStream);
+ }
+
+ IsarId = createQuery.IsarId;
+ Name = createQuery.Name;
+ SerialNumber = createQuery.SerialNumber;
+ CurrentInstallation = createQuery.CurrentInstallation;
+ CurrentArea = createQuery.CurrentArea;
+ VideoStreams = videoStreams;
+ Host = createQuery.Host;
+ Port = createQuery.Port;
+ Enabled = createQuery.Enabled;
+ Status = createQuery.Status;
+ Pose = new Pose();
+ }
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public string Id { get; set; }
@@ -46,6 +86,9 @@ public class Robot
[Required]
public bool Enabled { get; set; }
+ [Required]
+ public bool MissionQueueFrozen { get; set; }
+
[Required]
public RobotStatus Status { get; set; }
@@ -61,52 +104,13 @@ public string IsarUri
const string Method = "http";
string host = Host;
if (host == "0.0.0.0")
+ {
host = "localhost";
+ }
return $"{Method}://{host}:{Port}";
}
}
-
- public Robot()
- {
- VideoStreams = new List();
- IsarId = "defaultIsarId";
- Name = "defaultId";
- SerialNumber = "defaultSerialNumber";
- CurrentInstallation = "defaultAsset";
- Status = RobotStatus.Offline;
- Enabled = false;
- Host = "localhost";
- Port = 3000;
- Pose = new Pose();
- }
-
- public Robot(CreateRobotQuery createQuery)
- {
- var videoStreams = new List();
- foreach (var videoStreamQuery in createQuery.VideoStreams)
- {
- var videoStream = new VideoStream
- {
- Name = videoStreamQuery.Name,
- Url = videoStreamQuery.Url,
- Type = videoStreamQuery.Type
- };
- videoStreams.Add(videoStream);
- }
-
- IsarId = createQuery.IsarId;
- Name = createQuery.Name;
- SerialNumber = createQuery.SerialNumber;
- CurrentInstallation = createQuery.CurrentInstallation;
- CurrentArea = createQuery.CurrentArea;
- VideoStreams = videoStreams;
- Host = createQuery.Host;
- Port = createQuery.Port;
- Enabled = createQuery.Enabled;
- Status = createQuery.Status;
- Pose = new Pose();
- }
}
public enum RobotStatus
diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs
index fa3fa4ef2..7b6cfcea4 100644
--- a/backend/api/EventHandlers/MissionEventHandler.cs
+++ b/backend/api/EventHandlers/MissionEventHandler.cs
@@ -1,12 +1,11 @@
-using Api.Controllers;
-using Api.Controllers.Models;
+using Api.Controllers.Models;
using Api.Database.Models;
using Api.Services;
using Api.Services.Events;
using Api.Utilities;
-using Microsoft.AspNetCore.Mvc;
namespace Api.EventHandlers
{
+
public class MissionEventHandler : EventHandlerBase
{
private readonly ILogger _logger;
@@ -31,8 +30,11 @@ IServiceScopeFactory scopeFactory
private IRobotService RobotService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService();
- private RobotController RobotController =>
- _scopeFactory.CreateScope().ServiceProvider.GetRequiredService();
+ private IAreaService AreaService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService();
+
+ private IMissionScheduling MissionSchedulingService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService();
+
+ private IMqttEventHandler MqttEventHandlerService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService();
private IList MissionRunQueue(string robotId)
{
@@ -56,12 +58,16 @@ public override void Subscribe()
{
MissionRunService.MissionRunCreated += OnMissionRunCreated;
MqttEventHandler.RobotAvailable += OnRobotAvailable;
+ EmergencyActionService.EmergencyButtonPressedForRobot += OnEmergencyButtonPressedForRobot;
+ EmergencyActionService.EmergencyButtonDepressedForRobot += OnEmergencyButtonDepressedForRobot;
}
public override void Unsubscribe()
{
MissionRunService.MissionRunCreated -= OnMissionRunCreated;
MqttEventHandler.RobotAvailable -= OnRobotAvailable;
+ EmergencyActionService.EmergencyButtonPressedForRobot -= OnEmergencyButtonPressedForRobot;
+ EmergencyActionService.EmergencyButtonDepressedForRobot -= OnEmergencyButtonDepressedForRobot;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
@@ -81,14 +87,14 @@ private void OnMissionRunCreated(object? sender, MissionRunCreatedEventArgs e)
return;
}
- if (MissionRunQueueIsEmpty(MissionRunQueue(missionRun.Robot.Id)))
+ if (MissionScheduling.MissionRunQueueIsEmpty(MissionRunQueue(missionRun.Robot.Id)))
{
_logger.LogInformation("Mission run {MissionRunId} was not started as there are no mission runs on the queue", e.MissionRunId);
return;
}
_scheduleMissionMutex.WaitOne();
- StartMissionRunIfSystemIsAvailable(missionRun);
+ MissionSchedulingService.StartMissionRunIfSystemIsAvailable(missionRun);
_scheduleMissionMutex.ReleaseMutex();
}
@@ -102,111 +108,112 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e)
return;
}
- if (MissionRunQueueIsEmpty(MissionRunQueue(robot.Id)))
+ if (MissionScheduling.MissionRunQueueIsEmpty(MissionRunQueue(robot.Id)))
{
_logger.LogInformation("The robot was changed to available but there are no mission runs in the queue to be scheduled");
return;
}
- var missionRun = MissionRunQueue(robot.Id).First(missionRun => missionRun.Robot.Id == robot.Id);
+ var missionRun = (MissionRun?)null;
+
+ if (robot.MissionQueueFrozen == true)
+ {
+ missionRun = MissionRunQueue(robot.Id).FirstOrDefault(missionRun => missionRun.Robot.Id == robot.Id &&
+ missionRun.MissionRunPriority == MissionRunPriority.Emergency);
+ }
+ else
+ {
+ missionRun = MissionRunQueue(robot.Id).FirstOrDefault(missionRun => missionRun.Robot.Id == robot.Id);
+ }
+
+ if (missionRun == null)
+ {
+ _logger.LogInformation("The robot was changed to available but no mission is scheduled");
+ return;
+ }
_scheduleMissionMutex.WaitOne();
- StartMissionRunIfSystemIsAvailable(missionRun);
+ MissionSchedulingService.StartMissionRunIfSystemIsAvailable(missionRun);
_scheduleMissionMutex.ReleaseMutex();
}
- private void StartMissionRunIfSystemIsAvailable(MissionRun missionRun)
+ private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e)
{
- if (!TheSystemIsAvailableToRunAMission(missionRun.Robot, missionRun).Result)
+ _logger.LogInformation("Triggered EmergencyButtonPressed event for robot ID: {RobotId}", e.RobotId);
+ var robot = await RobotService.ReadById(e.RobotId);
+ if (robot == null)
{
- _logger.LogInformation("Mission {MissionRunId} was put on the queue as the system may not start a mission now", missionRun.Id);
+ _logger.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId);
return;
}
+ var area = await AreaService.ReadById(robot.CurrentArea!.Id);
+ if (area == null)
+ {
+ _logger.LogError("Could not find area with ID {AreaId}", robot.CurrentArea!.Id);
+ return;
+ }
+
+ await MissionSchedulingService.FreezeMissionRunQueueForRobot(e.RobotId);
+
try
{
- StartMissionRun(missionRun);
+ await MissionSchedulingService.StopCurrentMissionRun(e.RobotId);
}
catch (MissionException ex)
{
- const MissionStatus NewStatus = MissionStatus.Failed;
- _logger.LogWarning(
- "Mission run {MissionRunId} was not started successfully. Status updated to '{Status}'.\nReason: {FailReason}",
- missionRun.Id,
- NewStatus,
- ex.Message
- );
- missionRun.Status = NewStatus;
- missionRun.StatusReason = $"Failed to start: '{ex.Message}'";
- MissionService.Update(missionRun);
+ // We want to continue driving to a safe position if the isar state is idle
+ if (ex.IsarStatusCode != StatusCodes.Status409Conflict)
+ {
+ _logger.LogError(ex, "Failed to stop the current mission on robot {RobotName} because: {ErrorMessage}", robot.Name, ex.Message);
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ string message = "Error in ISAR while stopping current mission, cannot drive to safe position";
+ _logger.LogError(ex, "{Message}", message);
+ return;
}
- }
-
- private static bool MissionRunQueueIsEmpty(IList missionRunQueue)
- {
- return !missionRunQueue.Any();
- }
-
- private async Task TheSystemIsAvailableToRunAMission(Robot robot, MissionRun missionRun)
- {
- bool ongoingMission = await OngoingMission(robot.Id);
- if (ongoingMission)
+ try
{
- _logger.LogInformation("Mission run {MissionRunId} was not started as there is already an ongoing mission", missionRun.Id);
- return false;
+ await MissionSchedulingService.ScheduleMissionToReturnToSafePosition(e.RobotId, area.Id);
}
- if (robot.Status is not RobotStatus.Available)
+ catch (SafeZoneException ex)
{
- _logger.LogInformation("Mission run {MissionRunId} was not started as the robot is not available", missionRun.Id);
- return false;
+ _logger.LogError(ex, "Failed to schedule return to safe zone mission on robot {RobotName} because: {ErrorMessage}", robot.Name, ex.Message);
+ await MissionSchedulingService.UnfreezeMissionRunQueueForRobot(e.RobotId);
}
- if (!robot.Enabled)
+
+ MqttEventHandlerService.TriggerRobotAvailable(new RobotAvailableEventArgs(robot.Id));
+
+ }
+
+ private async void OnEmergencyButtonDepressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e)
+ {
+ _logger.LogInformation("Triggered EmergencyButtonPressed event for robot ID: {RobotId}", e.RobotId);
+ var robot = await RobotService.ReadById(e.RobotId);
+ if (robot == null)
{
- _logger.LogWarning("Mission run {MissionRunId} was not started as the robot {RobotId} is not enabled", missionRun.Id, robot.Id);
- return false;
+ _logger.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId);
+ return;
}
- if (missionRun.DesiredStartTime > DateTimeOffset.UtcNow)
+
+ var area = await AreaService.ReadById(robot.CurrentArea!.Id);
+ if (area == null)
{
- _logger.LogInformation("Mission run {MissionRunId} was not started as the start time is in the future", missionRun.Id);
- return false;
+ _logger.LogError("Could not find area with ID {AreaId}", robot.CurrentArea!.Id);
}
- return true;
- }
- private async Task OngoingMission(string robotId)
- {
- var ongoingMissions = await MissionService.ReadAll(
- new MissionRunQueryStringParameters
- {
- Statuses = new List
- {
- MissionStatus.Ongoing
- },
- RobotId = robotId,
- OrderBy = "DesiredStartTime",
- PageSize = 100
- });
-
- return ongoingMissions.Any();
- }
+ await MissionSchedulingService.UnfreezeMissionRunQueueForRobot(e.RobotId);
- private void StartMissionRun(MissionRun queuedMissionRun)
- {
- var result = RobotController.StartMission(
- queuedMissionRun.Robot.Id,
- queuedMissionRun.Id
- ).Result;
- if (result.Result is not OkObjectResult)
- {
- string errorMessage = "Unknown error from robot controller";
- if (result.Result is ObjectResult returnObject)
- {
- errorMessage = returnObject.Value?.ToString() ?? errorMessage;
- }
- throw new MissionException(errorMessage);
+ if (await MissionSchedulingService.OngoingMission(robot.Id))
+ {
+ _logger.LogInformation("Robot {RobotName} was unfrozen but the mission to return to safe zone will be completed before further missions are started", robot.Id);
}
- _logger.LogInformation("Started mission run '{Id}'", queuedMissionRun.Id);
+
+ MqttEventHandlerService.TriggerRobotAvailable(new RobotAvailableEventArgs(robot.Id));
}
}
}
diff --git a/backend/api/EventHandlers/MissionScheduling.cs b/backend/api/EventHandlers/MissionScheduling.cs
new file mode 100644
index 000000000..c5050a85f
--- /dev/null
+++ b/backend/api/EventHandlers/MissionScheduling.cs
@@ -0,0 +1,295 @@
+using System.Text.Json;
+using Api.Controllers.Models;
+using Api.Database.Models;
+using Api.Services;
+using Api.Utilities;
+namespace Api.EventHandlers
+{
+ public interface IMissionScheduling
+ {
+ public void StartMissionRunIfSystemIsAvailable(MissionRun missionRun);
+
+ public Task TheSystemIsAvailableToRunAMission(string robotId, MissionRun missionRun);
+
+ public Task OngoingMission(string robotId);
+
+ public Task?> GetOngoingMission(string robotId);
+
+ public Task FreezeMissionRunQueueForRobot(string robotId);
+
+ public Task StopCurrentMissionRun(string robotId);
+
+ public Task ScheduleMissionToReturnToSafePosition(string robotId, string areaId);
+
+ public Task UnfreezeMissionRunQueueForRobot(string robotId);
+
+ }
+
+ public class MissionScheduling : IMissionScheduling
+ {
+ private readonly IIsarService _isarService;
+ private readonly ILogger _logger;
+ private readonly IMissionRunService _missionRunService;
+ private readonly IAreaService _areaService;
+ private readonly IRobotService _robotService;
+ private readonly IMissionSchedulingService _missionSchedulingService;
+
+ public MissionScheduling(ILogger logger, IMissionRunService missionRunService, IIsarService isarService, IRobotService robotService, IAreaService areaService, IMissionSchedulingService missionSchedulingService)
+ {
+ _logger = logger;
+ _missionRunService = missionRunService;
+ _isarService = isarService;
+ _robotService = robotService;
+ _areaService = areaService;
+ _missionSchedulingService = missionSchedulingService;
+ }
+
+ public void StartMissionRunIfSystemIsAvailable(MissionRun missionRun)
+ {
+ if (!TheSystemIsAvailableToRunAMission(missionRun.Robot, missionRun).Result)
+ {
+ _logger.LogInformation("Mission {MissionRunId} was put on the queue as the system may not start a mission now", missionRun.Id);
+ return;
+ }
+
+ try
+ {
+ _missionSchedulingService.StartMissionRun(missionRun);
+ }
+ catch (MissionException ex)
+ {
+ const MissionStatus NewStatus = MissionStatus.Failed;
+ _logger.LogWarning(
+ "Mission run {MissionRunId} was not started successfully. Status updated to '{Status}'.\nReason: {FailReason}",
+ missionRun.Id,
+ NewStatus,
+ ex.Message
+ );
+ missionRun.Status = NewStatus;
+ missionRun.StatusReason = $"Failed to start: '{ex.Message}'";
+ _missionRunService.Update(missionRun);
+ }
+ }
+
+ public async Task TheSystemIsAvailableToRunAMission(string robotId, MissionRun missionRun)
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId);
+ return false;
+ }
+ return await TheSystemIsAvailableToRunAMission(robot, missionRun);
+ }
+
+ public async Task TheSystemIsAvailableToRunAMission(Robot robot, MissionRun missionRun)
+ {
+ bool ongoingMission = await OngoingMission(robot.Id);
+
+ if (robot.MissionQueueFrozen && missionRun.MissionRunPriority != MissionRunPriority.Emergency)
+ {
+ _logger.LogInformation("Mission run {MissionRunId} was not started as the mission run queue for robot {RobotName} is frozen", missionRun.Id, robot.Name);
+ return false;
+ }
+
+ if (ongoingMission)
+ {
+ _logger.LogInformation("Mission run {MissionRunId} was not started as there is already an ongoing mission", missionRun.Id);
+ return false;
+ }
+ if (robot.Status is not RobotStatus.Available)
+ {
+ _logger.LogInformation("Mission run {MissionRunId} was not started as the robot is not available", missionRun.Id);
+ return false;
+ }
+ if (!robot.Enabled)
+ {
+ _logger.LogWarning("Mission run {MissionRunId} was not started as the robot {RobotId} is not enabled", missionRun.Id, robot.Id);
+ return false;
+ }
+ if (missionRun.DesiredStartTime > DateTimeOffset.UtcNow)
+ {
+ _logger.LogInformation("Mission run {MissionRunId} was not started as the start time is in the future", missionRun.Id);
+ return false;
+ }
+ return true;
+ }
+
+ public async Task OngoingMission(string robotId)
+ {
+ var ongoingMissions = await _missionRunService.ReadAll(
+ new MissionRunQueryStringParameters
+ {
+ Statuses = new List
+ {
+ MissionStatus.Ongoing
+ },
+ RobotId = robotId,
+ OrderBy = "DesiredStartTime",
+ PageSize = 100
+ });
+
+ return ongoingMissions.Any();
+ }
+
+ public async Task?> GetOngoingMission(string robotId)
+ {
+ var ongoingMissions = await _missionRunService.ReadAll(
+ new MissionRunQueryStringParameters
+ {
+ Statuses = new List
+ {
+ MissionStatus.Ongoing
+ },
+ RobotId = robotId,
+ OrderBy = "DesiredStartTime",
+ PageSize = 100
+ });
+
+ return ongoingMissions;
+ }
+
+
+ public async Task FreezeMissionRunQueueForRobot(string robotId)
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId);
+ return;
+ }
+ robot.MissionQueueFrozen = true;
+ await _robotService.Update(robot);
+ _logger.LogInformation("Mission queue for robot {RobotName} with ID {RobotId} was frozen", robot.Name, robot.Id);
+ }
+
+ public async Task UnfreezeMissionRunQueueForRobot(string robotId)
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId);
+ return;
+ }
+ robot.MissionQueueFrozen = false;
+ await _robotService.Update(robot);
+ _logger.LogInformation("Mission queue for robot {RobotName} with ID {RobotId} was unfrozen", robot.Name, robot.Id);
+ }
+
+ public async Task StopCurrentMissionRun(string robotId)
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId);
+ return;
+ }
+
+ var ongoingMissions = await GetOngoingMission(robot.Id);
+
+ if (ongoingMissions == null)
+ {
+ _logger.LogWarning("Flotilla has no mission running for robot {RobotName} but an attempt to stop will be made regardless", robot.Name);
+ }
+ else
+ {
+ foreach (var mission in ongoingMissions)
+ {
+ if (mission.MissionRunPriority == MissionRunPriority.Emergency) continue;
+
+ var newMission = new MissionRun
+ {
+ Name = mission.Name,
+ Robot = robot,
+ MissionRunPriority = MissionRunPriority.Normal,
+ InstallationCode = mission.InstallationCode,
+ Area = mission.Area,
+ Status = MissionStatus.Pending,
+ DesiredStartTime = DateTimeOffset.UtcNow,
+ Tasks = mission.Tasks,
+ Map = new MapMetadata()
+ };
+
+ await _missionRunService.Create(newMission);
+ }
+ }
+
+ try
+ {
+ await _isarService.StopMission(robot);
+ }
+ catch (HttpRequestException e)
+ {
+ string message = "Error connecting to ISAR while stopping mission";
+ _logger.LogError(e, "{Message}", message);
+ _missionSchedulingService.OnIsarUnavailable(robot.Id);
+ throw new MissionException(message, (int)e.StatusCode!);
+ }
+ catch (MissionException e)
+ {
+ string message = "Error while stopping ISAR mission";
+ _logger.LogError(e, "{Message}", message);
+ throw;
+ }
+ catch (JsonException e)
+ {
+ string message = "Error while processing the response from ISAR";
+ _logger.LogError(e, "{Message}", message);
+ throw new MissionException(message, 0);
+ }
+
+ robot.CurrentMissionId = null;
+ await _robotService.Update(robot);
+ }
+
+ public async Task ScheduleMissionToReturnToSafePosition(string robotId, string areaId)
+ {
+ var area = await _areaService.ReadById(areaId);
+ if (area == null)
+ {
+ _logger.LogError("Could not find area with ID {AreaId}", areaId);
+ return;
+ }
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId);
+ return;
+ }
+ var closestSafePosition = _missionSchedulingService.ClosestSafePosition(robot.Pose, area.SafePositions);
+ // Cloning to avoid tracking same object
+ var clonedPose = ObjectCopier.Clone(closestSafePosition);
+ var customTaskQuery = new CustomTaskQuery
+ {
+ RobotPose = clonedPose,
+ Inspections = new List(),
+ InspectionTarget = new Position(),
+ TaskOrder = 0
+ };
+
+ var missionRun = new MissionRun
+ {
+ Name = "Drive to Safe Position",
+ Robot = robot,
+ MissionRunPriority = MissionRunPriority.Emergency,
+ InstallationCode = area.Installation!.InstallationCode,
+ Area = area,
+ Status = MissionStatus.Pending,
+ DesiredStartTime = DateTimeOffset.UtcNow,
+ Tasks = new List(new[]
+ {
+ new MissionTask(customTaskQuery)
+ }),
+ Map = new MapMetadata()
+ };
+
+ await _missionRunService.Create(missionRun);
+ }
+
+ public static bool MissionRunQueueIsEmpty(IList missionRunQueue)
+ {
+ return !missionRunQueue.Any();
+ }
+
+ }
+}
diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs
index 9f12da93b..4751240f9 100644
--- a/backend/api/EventHandlers/MqttEventHandler.cs
+++ b/backend/api/EventHandlers/MqttEventHandler.cs
@@ -16,7 +16,13 @@ namespace Api.EventHandlers
///
/// A background service which listens to events and performs callback functions.
///
- public class MqttEventHandler : EventHandlerBase
+ ///
+ public interface IMqttEventHandler
+ {
+ public void TriggerRobotAvailable(RobotAvailableEventArgs e);
+ }
+
+ public class MqttEventHandler : EventHandlerBase, IMqttEventHandler
{
private readonly ILogger _logger;
private readonly IServiceScopeFactory _scopeFactory;
@@ -58,7 +64,15 @@ public override void Unsubscribe()
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await stoppingToken; }
- protected virtual void OnRobotAvailable(RobotAvailableEventArgs e) { RobotAvailable?.Invoke(this, e); }
+ public void TriggerRobotAvailable(RobotAvailableEventArgs e)
+ {
+ OnRobotAvailable(e);
+ }
+
+ protected virtual void OnRobotAvailable(RobotAvailableEventArgs e)
+ {
+ RobotAvailable?.Invoke(this, e);
+ }
public static event EventHandler? RobotAvailable;
@@ -229,14 +243,13 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs)
await robotService.Update(robot);
_logger.LogInformation("Robot '{Id}' ('{Name}') - completed mission {MissionId}", robot.IsarId, robot.Name, flotillaMissionRun.MissionId);
- if (!flotillaMissionRun.IsCompleted) { return; }
-
+ if (!flotillaMissionRun.IsCompleted) return;
await taskDurationService.UpdateAverageDurationPerTask(robot.Model.Type);
- if (flotillaMissionRun.MissionId == null) { return; }
+ if (flotillaMissionRun.MissionId == null) return;
var missionDefinition = await missionDefinitionService.ReadById(flotillaMissionRun.MissionId);
- if (missionDefinition == null) { return; }
+ if (missionDefinition == null) return;
missionDefinition.LastRun = flotillaMissionRun;
await missionDefinitionService.Update(missionDefinition);
diff --git a/backend/api/Program.cs b/backend/api/Program.cs
index dc2de23a8..80dbce91f 100644
--- a/backend/api/Program.cs
+++ b/backend/api/Program.cs
@@ -50,6 +50,8 @@
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
@@ -64,8 +66,10 @@
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddScoped();
+
bool useInMemoryDatabase = builder.Configuration
.GetSection("Database")
.GetValue("UseInMemoryDatabase");
@@ -79,7 +83,9 @@
builder.Services.AddScoped();
}
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddHostedService();
builder.Services.AddHostedService();
diff --git a/backend/api/Services/ActionServices/MissionSchedulingService.cs b/backend/api/Services/ActionServices/CustomMissionSchedulingService.cs
similarity index 95%
rename from backend/api/Services/ActionServices/MissionSchedulingService.cs
rename to backend/api/Services/ActionServices/CustomMissionSchedulingService.cs
index 0b13f36db..90776a15a 100644
--- a/backend/api/Services/ActionServices/MissionSchedulingService.cs
+++ b/backend/api/Services/ActionServices/CustomMissionSchedulingService.cs
@@ -3,14 +3,14 @@
using Api.Utilities;
namespace Api.Services.ActionServices
{
- public interface IMissionSchedulingService
+ public interface ICustomMissionSchedulingService
{
public Task FindExistingOrCreateCustomMissionDefinition(CustomMissionQuery customMissionQuery, List missionTasks);
public Task QueueCustomMissionRun(CustomMissionQuery customMissionQuery, string missionDefinitionId, string robotId, IList missionTasks);
}
- public class MissionSchedulingService : IMissionSchedulingService
+ public class CustomMissionSchedulingService : ICustomMissionSchedulingService
{
private readonly IAreaService _areaService;
private readonly ICustomMissionService _customMissionService;
@@ -21,7 +21,7 @@ public class MissionSchedulingService : IMissionSchedulingService
private readonly IRobotService _robotService;
private readonly ISourceService _sourceService;
- public MissionSchedulingService(
+ public CustomMissionSchedulingService(
ILogger logger,
ICustomMissionService customMissionService,
IAreaService areaService,
@@ -117,6 +117,7 @@ public async Task QueueCustomMissionRun(CustomMissionQuery customMis
Comment = customMissionQuery.Comment,
Robot = robot,
Status = MissionStatus.Pending,
+ MissionRunPriority = MissionRunPriority.Normal,
DesiredStartTime = customMissionQuery.DesiredStartTime ?? DateTimeOffset.UtcNow,
Tasks = missionTasks,
InstallationCode = customMissionQuery.InstallationCode,
diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs
index c555c9ab8..fc685567a 100644
--- a/backend/api/Services/AreaService.cs
+++ b/backend/api/Services/AreaService.cs
@@ -93,7 +93,7 @@ private IQueryable GetAreas()
return await _context.Areas.Where(a =>
a.Name.ToLower().Equals(areaName.ToLower()) &&
- a.Installation != null && a.Installation.Id.Equals(installation.Id)
+ a.Installation.InstallationCode.Equals(installation.InstallationCode)
).Include(a => a.SafePositions).Include(a => a.Installation)
.Include(a => a.Plant).Include(a => a.Deck).FirstOrDefaultAsync();
}
diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs
index c45236696..2198b8c89 100644
--- a/backend/api/Services/EchoService.cs
+++ b/backend/api/Services/EchoService.cs
@@ -137,7 +137,8 @@ private List ProcessPlanItems(List planItems, string installa
$"https://stid.equinor.com/{installationCode}/tag?tagNo={planItem.Tag}"
),
Inspections = planItem.SensorTypes
- .Select(sensor => new EchoInspection(sensor)).Distinct(new EchoInspectionComparer()).ToList()
+ .Select(sensor => new EchoInspection(sensor))
+ .ToList()
};
if (tag.Inspections.IsNullOrEmpty())
diff --git a/backend/api/Services/EmergencyActionService.cs b/backend/api/Services/EmergencyActionService.cs
new file mode 100644
index 000000000..d35cb3856
--- /dev/null
+++ b/backend/api/Services/EmergencyActionService.cs
@@ -0,0 +1,42 @@
+using Api.Services.Events;
+namespace Api.Services
+{
+ public interface IEmergencyActionService
+ {
+ public void TriggerEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobotEventArgs e);
+
+ public void TriggerEmergencyButtonDepressedForRobot(EmergencyButtonPressedForRobotEventArgs e);
+ }
+
+ public class EmergencyActionService : IEmergencyActionService
+ {
+
+ public EmergencyActionService()
+ {
+ }
+
+ public void TriggerEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobotEventArgs e)
+ {
+ OnEmergencyButtonPressedForRobot(e);
+ }
+
+ public void TriggerEmergencyButtonDepressedForRobot(EmergencyButtonPressedForRobotEventArgs e)
+ {
+ OnEmergencyButtonDepressedForRobot(e);
+ }
+
+ public static event EventHandler? EmergencyButtonPressedForRobot;
+
+ protected virtual void OnEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobotEventArgs e)
+ {
+ EmergencyButtonPressedForRobot?.Invoke(this, e);
+ }
+
+ public static event EventHandler? EmergencyButtonDepressedForRobot;
+
+ protected virtual void OnEmergencyButtonDepressedForRobot(EmergencyButtonPressedForRobotEventArgs e)
+ {
+ EmergencyButtonDepressedForRobot?.Invoke(this, e);
+ }
+ }
+}
diff --git a/backend/api/Services/Events/MissionEventArgs.cs b/backend/api/Services/Events/MissionEventArgs.cs
index def250d43..7c3f3e0f1 100644
--- a/backend/api/Services/Events/MissionEventArgs.cs
+++ b/backend/api/Services/Events/MissionEventArgs.cs
@@ -19,4 +19,15 @@ public RobotAvailableEventArgs(string robotId)
}
public string RobotId { get; set; }
}
+
+ public class EmergencyButtonPressedForRobotEventArgs : EventArgs
+ {
+ public EmergencyButtonPressedForRobotEventArgs(string robotId)
+ {
+ RobotId = robotId;
+ }
+
+ public string RobotId { get; set; }
+
+ }
}
diff --git a/backend/api/Services/MissionSchedulingService.cs b/backend/api/Services/MissionSchedulingService.cs
new file mode 100644
index 000000000..fbbe9f6d2
--- /dev/null
+++ b/backend/api/Services/MissionSchedulingService.cs
@@ -0,0 +1,106 @@
+using Api.Controllers;
+using Api.Database.Models;
+using Api.Utilities;
+using Microsoft.AspNetCore.Mvc;
+namespace Api.Services
+{
+ public interface IMissionSchedulingService
+ {
+ public void StartMissionRun(MissionRun queuedMissionRun);
+ public Pose ClosestSafePosition(Pose robotPose, IList safePositions);
+ public void OnIsarUnavailable(string robotId);
+ }
+
+ public class MissionSchedulingService : IMissionSchedulingService
+ {
+ private readonly ILogger _logger;
+ private readonly IMissionRunService _missionRunService;
+ private readonly RobotController _robotController;
+ private readonly IRobotService _robotService;
+
+ public MissionSchedulingService(ILogger logger, IMissionRunService missionRunService, IRobotService robotService, RobotController robotController)
+ {
+ _logger = logger;
+ _missionRunService = missionRunService;
+ _robotService = robotService;
+ _robotController = robotController;
+ }
+
+ public void StartMissionRun(MissionRun queuedMissionRun)
+ {
+ var result = _robotController.StartMission(
+ queuedMissionRun.Robot.Id,
+ queuedMissionRun.Id
+ ).Result;
+ if (result.Result is not OkObjectResult)
+ {
+ string errorMessage = "Unknown error from robot controller";
+ if (result.Result is ObjectResult returnObject)
+ {
+ errorMessage = returnObject.Value?.ToString() ?? errorMessage;
+ }
+ throw new MissionException(errorMessage);
+ }
+ _logger.LogInformation("Started mission run '{Id}'", queuedMissionRun.Id);
+ }
+
+ public async void OnIsarUnavailable(string robotId)
+ {
+ var robot = await _robotService.ReadById(robotId);
+ if (robot == null)
+ {
+ _logger.LogError("Robot with ID: {RobotId} was not found in the database", robotId);
+ return;
+ }
+
+ robot.Enabled = false;
+ robot.Status = RobotStatus.Offline;
+ if (robot.CurrentMissionId != null)
+ {
+ var missionRun = await _missionRunService.ReadById(robot.CurrentMissionId);
+ if (missionRun != null)
+ {
+ missionRun.SetToFailed();
+ await _missionRunService.Update(missionRun);
+ _logger.LogWarning(
+ "Mission '{Id}' failed because ISAR could not be reached",
+ missionRun.Id
+ );
+ }
+ }
+ robot.CurrentMissionId = null;
+ await _robotService.Update(robot);
+ }
+
+ public 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);
+ }
+
+ var closestPose = safePositions[0].Pose;
+ float minDistance = CalculateDistance(robotPose, closestPose);
+
+ for (int i = 1; i < safePositions.Count; i++)
+ {
+ float currentDistance = CalculateDistance(robotPose, safePositions[i].Pose);
+ if (currentDistance < minDistance)
+ {
+ minDistance = currentDistance;
+ closestPose = safePositions[i].Pose;
+ }
+ }
+ return closestPose;
+ }
+
+ private static float CalculateDistance(Pose pose1, Pose pose2)
+ {
+ var pos1 = pose1.Position;
+ var pos2 = pose2.Position;
+ return (float)Math.Sqrt(Math.Pow(pos1.X - pos2.X, 2) + Math.Pow(pos1.Y - pos2.Y, 2) + Math.Pow(pos1.Z - pos2.Z, 2));
+ }
+
+ }
+}
diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs
index bfa50f537..f2459b2b4 100644
--- a/backend/api/Services/RobotService.cs
+++ b/backend/api/Services/RobotService.cs
@@ -101,7 +101,11 @@ public async Task Update(Robot robot)
private IQueryable GetRobotsWithSubModels()
{
- return _context.Robots.Include(r => r.VideoStreams).Include(r => r.Model).Include(r => r.CurrentArea);
+ return _context.Robots
+ .Include(r => r.VideoStreams)
+ .Include(r => r.Model)
+ .Include(r => r.CurrentArea)
+ .ThenInclude(r => r != null ? r.SafePositions : null);
}
}
}
diff --git a/backend/api/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs
index 63012a333..f96d81710 100644
--- a/backend/api/Utilities/Exceptions.cs
+++ b/backend/api/Utilities/Exceptions.cs
@@ -80,4 +80,9 @@ public class DeckExistsException : Exception
{
public DeckExistsException(string message) : base(message) { }
}
+
+ public class SafeZoneException : Exception
+ {
+ public SafeZoneException(string message) : base(message) { }
+ }
}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index e272e90ac..b2c76953a 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -5,27 +5,30 @@ import { LanguageProvider } from 'components/Contexts/LanguageContext'
import { MissionControlProvider } from 'components/Contexts/MissionControlContext'
import { MissionFilterProvider } from 'components/Contexts/MissionFilterContext'
import { MissionsProvider } from 'components/Contexts/MissionListsContext'
+import { SafeZoneProvider } from 'components/Contexts/SafeZoneContext'
function App() {
return (
-
-
-
- <>
-
-
-
-
-
-
-
-
- >
-
-
-
+
+
+
+
+ <>
+
+
+
+
+
+
+
+
+ >
+
+
+
+
)
}
diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx
index bcce63977..81958fa70 100644
--- a/frontend/src/api/ApiCaller.tsx
+++ b/frontend/src/api/ApiCaller.tsx
@@ -481,4 +481,26 @@ export class BackendAPICaller {
})
return result.content
}
+
+ static async sendRobotsToSafePosition(installationCode: string) {
+ const path: string = `emergency-action/${installationCode}/abort-current-missions-and-send-all-robots-to-safe-zone`
+ const body = {}
+
+ const result = await this.POST(path, body).catch((e) => {
+ console.error(`Failed to POST /${path}: ` + e)
+ throw e
+ })
+ return result.content
+ }
+
+ static async clearEmergencyState(installationCode: string) {
+ const path: string = `emergency-action/${installationCode}/clear-emergency-state`
+ const body = {}
+
+ const result = await this.POST(path, body).catch((e) => {
+ console.error(`Failed to POST /${path}: ` + e)
+ throw e
+ })
+ return result.content
+ }
}
diff --git a/frontend/src/components/Contexts/MissionControlContext.tsx b/frontend/src/components/Contexts/MissionControlContext.tsx
index 28b193d56..fedb6cd20 100644
--- a/frontend/src/components/Contexts/MissionControlContext.tsx
+++ b/frontend/src/components/Contexts/MissionControlContext.tsx
@@ -1,7 +1,7 @@
import { createContext, useContext, useState, FC } from 'react'
import { BackendAPICaller } from 'api/ApiCaller'
import { Mission } from 'models/Mission'
-import { MissionStatusRequest } from 'components/Pages/FrontPage/MissionOverview/StopMissionDialog'
+import { MissionStatusRequest } from 'components/Pages/FrontPage/MissionOverview/StopDialogs'
interface IMissionControlState {
isWaitingForResponse: boolean
diff --git a/frontend/src/components/Contexts/SafeZoneContext.tsx b/frontend/src/components/Contexts/SafeZoneContext.tsx
new file mode 100644
index 000000000..5fa4fc05a
--- /dev/null
+++ b/frontend/src/components/Contexts/SafeZoneContext.tsx
@@ -0,0 +1,40 @@
+import { BackendAPICaller } from 'api/ApiCaller'
+import { createContext, FC, useContext, useEffect, useState } from 'react'
+
+interface ISafeZoneContext {
+ safeZoneStatus: boolean
+ switchSafeZoneStatus: (newSafeZoneStatus: boolean) => void
+}
+
+interface Props {
+ children: React.ReactNode
+}
+
+const defaultSafeZoneInterface = {
+ safeZoneStatus: JSON.parse(localStorage.getItem('safeZoneStatus') ?? 'false'),
+ switchSafeZoneStatus: (newSafeZoneStatus: boolean) => {},
+}
+
+export const SafeZoneContext = createContext(defaultSafeZoneInterface)
+
+export const SafeZoneProvider: FC = ({ children }) => {
+ const [safeZoneStatus, setSafeZoneStatus] = useState(defaultSafeZoneInterface.safeZoneStatus)
+
+ const switchSafeZoneStatus = (newSafeZoneStatus: boolean) => {
+ localStorage.setItem('safeZoneStatus', String(newSafeZoneStatus))
+ setSafeZoneStatus(newSafeZoneStatus)
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useSafeZoneContext = () => useContext(SafeZoneContext)
diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionControlButtons.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionControlButtons.tsx
index c0233f4bd..6ef39d35f 100644
--- a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionControlButtons.tsx
+++ b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionControlButtons.tsx
@@ -6,7 +6,7 @@ import styled from 'styled-components'
import { Typography } from '@equinor/eds-core-react'
import { useLanguageContext } from 'components/Contexts/LanguageContext'
import { useMissionControlContext } from 'components/Contexts/MissionControlContext'
-import { StopMissionDialog, MissionStatusRequest } from './StopMissionDialog'
+import { StopMissionDialog, MissionStatusRequest } from './StopDialogs'
interface MissionProps {
mission: Mission
diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx
index 1e3be4b3d..3954d1a2c 100644
--- a/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx
+++ b/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx
@@ -9,6 +9,7 @@ import { config } from 'config'
import { Icons } from 'utils/icons'
/* import { useOngoingMissionsContext } from 'components/Contexts/OngoingMissionsContext' */
import { useMissionsContext } from 'components/Contexts/MissionListsContext'
+import { StopRobotDialog } from './StopDialogs'
const StyledOngoingMissionView = styled.div`
display: flex;
@@ -25,6 +26,12 @@ const ButtonStyle = styled.div`
display: block;
`
+const OngoingMissionHeader = styled.div`
+ display: grid;
+ grid-direction: column;
+ gap: 0.5rem;
+`
+
export function OngoingMissionView({ refreshInterval }: RefreshProps) {
const { TranslateText } = useLanguageContext()
const { ongoingMissions } = useMissionsContext()
@@ -40,9 +47,12 @@ export function OngoingMissionView({ refreshInterval }: RefreshProps) {
return (
-
- {TranslateText('Ongoing Missions')}
-
+
+
+ {TranslateText('Ongoing Missions')}
+
+
+
{ongoingMissions.length > 0 && ongoingMissionscard}
{ongoingMissions.length === 0 && }
diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx
new file mode 100644
index 000000000..774e741c3
--- /dev/null
+++ b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx
@@ -0,0 +1,231 @@
+import { Button, Dialog, Typography, Icon } from '@equinor/eds-core-react'
+import styled from 'styled-components'
+import { useLanguageContext } from 'components/Contexts/LanguageContext'
+import { Icons } from 'utils/icons'
+import { useState, useEffect } from 'react'
+import { tokens } from '@equinor/eds-tokens'
+import { Mission } from 'models/Mission'
+import { useMissionControlContext } from 'components/Contexts/MissionControlContext'
+import { BackendAPICaller } from 'api/ApiCaller'
+import { useInstallationContext } from 'components/Contexts/InstallationContext'
+import { useSafeZoneContext } from 'components/Contexts/SafeZoneContext'
+
+const StyledDisplayButtons = styled.div`
+ display: flex;
+ width: 410px;
+ flex-direction: columns;
+ justify-content: flex-end;
+ gap: 0.5rem;
+`
+
+const StyledDialog = styled(Dialog)`
+ display: grid;
+ width: 450px;
+`
+
+const StyledText = styled.div`
+ display: grid;
+ gird-template-rows: auto, auto;
+ gap: 1rem;
+`
+
+const StyledButton = styled.div`
+ width: 250px;
+`
+
+const Square = styled.div`
+ width: 12px;
+ height: 12px;
+`
+
+interface MissionProps {
+ mission: Mission
+}
+
+export enum MissionStatusRequest {
+ Pause,
+ Stop,
+ Resume,
+}
+
+export const StopMissionDialog = ({ mission }: MissionProps): JSX.Element => {
+ const { TranslateText } = useLanguageContext()
+ const [isStopMissionDialogOpen, setIsStopMissionDialogOpen] = useState(false)
+ const [missionId, setMissionId] = useState()
+ const { updateMissionState } = useMissionControlContext()
+
+ const openDialog = () => {
+ setIsStopMissionDialogOpen(true)
+ setMissionId(mission.id)
+ }
+
+ useEffect(() => {
+ if (missionId !== mission.id) setIsStopMissionDialogOpen(false)
+ }, [mission.id])
+
+ return (
+ <>
+
+
+
+
+
+
+ {TranslateText('Stop mission')} '{mission.name}'?{' '}
+
+
+
+
+
+ {TranslateText('Stop button pressed warning text')}
+
+ {TranslateText('Stop button pressed confirmation text')}
+
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+export const StopRobotDialog = (): JSX.Element => {
+ const [isStopRobotDialogOpen, setIsStopRobotDialogOpen] = useState(false)
+ const { safeZoneStatus } = useSafeZoneContext()
+ const { TranslateText } = useLanguageContext()
+ const { installationCode } = useInstallationContext()
+
+ const openDialog = async () => {
+ setIsStopRobotDialogOpen(true)
+ }
+
+ const closeDialog = async () => {
+ setIsStopRobotDialogOpen(false)
+ }
+
+ const stopAll = () => {
+ BackendAPICaller.sendRobotsToSafePosition(installationCode)
+ closeDialog()
+ return
+ }
+
+ const resetRobots = () => {
+ BackendAPICaller.clearEmergencyState(installationCode)
+ closeDialog()
+ }
+
+ return (
+ <>
+ {!safeZoneStatus && (
+ <>
+
+
+
+
+
+
+ {TranslateText('Send robots to safe zone') + '?'}
+
+
+
+
+
+ {TranslateText('Send robots to safe zone long text')}
+
+
+ {TranslateText('Send robots to safe confirmation text')}
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ {safeZoneStatus && (
+ <>
+
+
+
+
+
+
+
+ {TranslateText('Dismiss robots from safe zone') + '?'}
+
+
+
+
+
+
+ {TranslateText('Dismiss robots from safe zone long text')}
+
+
+
+
+
+
+
+
+
+
+ >
+ )}
+ >
+ )
+}
diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/StopMissionDialog.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/StopMissionDialog.tsx
deleted file mode 100644
index dcb9f186d..000000000
--- a/frontend/src/components/Pages/FrontPage/MissionOverview/StopMissionDialog.tsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { Button, Dialog, Typography, Icon } from '@equinor/eds-core-react'
-import styled from 'styled-components'
-import { useLanguageContext } from 'components/Contexts/LanguageContext'
-import { Icons } from 'utils/icons'
-import { useState, useEffect } from 'react'
-import { tokens } from '@equinor/eds-tokens'
-import { Mission } from 'models/Mission'
-import { useMissionControlContext } from 'components/Contexts/MissionControlContext'
-
-const StyledDisplayButtons = styled.div`
- display: flex;
- width: 410px;
- flex-direction: columns;
- justify-content: flex-end;
- gap: 0.5rem;
-`
-
-const StyledDialog = styled(Dialog)`
- display: grid;
- width: 450px;
-`
-
-const StyledText = styled.div`
- display: grid;
- gird-template-rows: auto, auto;
- gap: 1rem;
-`
-
-interface MissionProps {
- mission: Mission
-}
-
-export enum MissionStatusRequest {
- Pause,
- Stop,
- Resume,
-}
-
-export const StopMissionDialog = ({ mission }: MissionProps): JSX.Element => {
- const { TranslateText } = useLanguageContext()
- const { updateMissionState } = useMissionControlContext()
- const [isStopMissionDialogOpen, setIsStopMissionDialogOpen] = useState(false)
- const [missionId, setMissionId] = useState()
-
- const openDialog = () => {
- setIsStopMissionDialogOpen(true)
- setMissionId(mission.id)
- }
-
- useEffect(() => {
- if (missionId !== mission.id) setIsStopMissionDialogOpen(false)
- }, [mission.id, missionId])
-
- return (
- <>
-
-
-
-
-
-
- {TranslateText('Stop mission')} '{mission.name}'?{' '}
-
-
-
-
-
- {TranslateText('Stop button pressed warning text')}
-
- {TranslateText('Stop button pressed confirmation text')}
-
-
-
-
-
-
-
-
-
-
- >
- )
-}
diff --git a/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusChip.tsx b/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusChip.tsx
index 75d5588c0..c9cf3543b 100644
--- a/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusChip.tsx
+++ b/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusChip.tsx
@@ -2,6 +2,7 @@ import { Chip } from '@equinor/eds-core-react'
import { RobotStatus } from 'models/Robot'
import { tokens } from '@equinor/eds-tokens'
import { useLanguageContext } from 'components/Contexts/LanguageContext'
+import { useSafeZoneContext } from 'components/Contexts/SafeZoneContext'
interface StatusProps {
status?: RobotStatus
@@ -12,11 +13,14 @@ enum StatusColors {
Offline = '#F7F7F7',
Busy = '#FFC67A',
Blocked = '#FFC67A',
+ SafeZone = '#FF0000',
}
export function RobotStatusChip({ status }: StatusProps) {
const { TranslateText } = useLanguageContext()
- let chipColor = StatusColors.Offline
+ const { safeZoneStatus } = useSafeZoneContext()
+
+ var chipColor = StatusColors.Offline
switch (status) {
case RobotStatus.Available: {
chipColor = StatusColors.Available
@@ -36,6 +40,12 @@ export function RobotStatusChip({ status }: StatusProps) {
break
}
}
+
+ if (safeZoneStatus) {
+ chipColor = StatusColors.SafeZone
+ status = RobotStatus.SafeZone
+ }
+
return (
([])
+ const { safeZoneStatus, switchSafeZoneStatus } = useSafeZoneContext()
const sortRobotsByStatus = useCallback((robots: Robot[]): Robot[] => {
const sortedRobots = robots.sort((robot, robotToCompareWith) =>
robot.status! > robotToCompareWith.status! ? 1 : -1
)
-
return sortedRobots
}, [])
const updateRobots = useCallback(() => {
BackendAPICaller.getEnabledRobots().then((result: Robot[]) => {
setRobots(sortRobotsByStatus(result))
+ const missionQueueFozenStatus = result
+ .map((robot: Robot) => {
+ return robot.missionQueueFrozen
+ })
+ .filter((status) => status === true)
+
+ if (missionQueueFozenStatus.length > 0 && !safeZoneStatus) switchSafeZoneStatus(true)
+ else switchSafeZoneStatus(false)
})
}, [sortRobotsByStatus])
diff --git a/frontend/src/components/Pages/InspectionPage/InspectionTable.tsx b/frontend/src/components/Pages/InspectionPage/InspectionTable.tsx
index 050cb9349..1678c5e5f 100644
--- a/frontend/src/components/Pages/InspectionPage/InspectionTable.tsx
+++ b/frontend/src/components/Pages/InspectionPage/InspectionTable.tsx
@@ -193,7 +193,7 @@ const InspectionRow = ({
{mission.comment}
- {mission.area.areaName}
+ {mission.area ? mission.area.areaName : '-'}
{lastCompleted}
{inspection.deadline ? formatDateString(inspection.deadline.toISOString()) : ''}
diff --git a/frontend/src/components/Pages/MissionDefinitionPage/MissionDefinitionPage.tsx b/frontend/src/components/Pages/MissionDefinitionPage/MissionDefinitionPage.tsx
index d45faef99..95e353010 100644
--- a/frontend/src/components/Pages/MissionDefinitionPage/MissionDefinitionPage.tsx
+++ b/frontend/src/components/Pages/MissionDefinitionPage/MissionDefinitionPage.tsx
@@ -79,8 +79,8 @@ function MissionDefinitionPageBody({ missionDefinition, updateMissionDefinition
const { TranslateText } = useLanguageContext()
let navigate = useNavigate()
- const displayInspectionFrequency = (inspectionFrequency: string) => {
- if (inspectionFrequency === null) return TranslateText('No inspection frequency set')
+ const displayInspectionFrequency = (inspectionFrequency: string | undefined) => {
+ if (inspectionFrequency === undefined) return TranslateText('No inspection frequency set')
const timeArray = inspectionFrequency.split(':')
const days: number = +timeArray[0]
const hours: number = +timeArray[1]
@@ -101,10 +101,22 @@ function MissionDefinitionPageBody({ missionDefinition, updateMissionDefinition
left={TranslateText('Inspection frequency')}
right={displayInspectionFrequency(missionDefinition.inspectionFrequency)}
/>
-
-
-
-
+
+
+
+
{
}
export const getInspectionDeadline = (
- inspectionFrequency: string | null,
+ inspectionFrequency: string | undefined,
lastRunTime: Date | null
): Date | undefined => {
if (!inspectionFrequency || !lastRunTime) return undefined
diff --git a/frontend/src/utils/icons.tsx b/frontend/src/utils/icons.tsx
index 8c746120d..5933f671e 100644
--- a/frontend/src/utils/icons.tsx
+++ b/frontend/src/utils/icons.tsx
@@ -37,6 +37,7 @@ import {
settings,
platform,
library_add,
+ play,
} from '@equinor/eds-icons'
Icon.add({
@@ -77,6 +78,7 @@ Icon.add({
settings,
platform,
library_add,
+ play,
})
export enum Icons {
@@ -117,4 +119,5 @@ export enum Icons {
Settings = 'settings',
Platform = 'platform',
LibraryAdd = 'library_add',
+ PlayTriangle = 'play',
}