From 1130a2a7a997126097985edd769c1a41b3ababf9 Mon Sep 17 00:00:00 2001 From: aestene Date: Fri, 18 Aug 2023 12:45:22 +0200 Subject: [PATCH 01/11] Add stop all button functionality --- .../EventHandlers/TestMissionEventHandler.cs | 23 +- .../Controllers/EmergencyActionController.cs | 104 ++ backend/api/Controllers/MissionController.cs | 9 +- backend/api/Controllers/RobotController.cs | 1532 ++++++++--------- backend/api/Database/Models/MissionRun.cs | 59 +- backend/api/Database/Models/Robot.cs | 88 +- .../EmergencyActionEventHandler.cs | 37 + .../api/EventHandlers/MissionEventHandler.cs | 135 +- .../api/EventHandlers/MissionScheduling.cs | 264 +++ backend/api/EventHandlers/MqttEventHandler.cs | 13 +- backend/api/Program.cs | 2 + .../api/Services/EmergencyActionService.cs | 46 + .../api/Services/Events/MissionEventArgs.cs | 13 + 13 files changed, 1320 insertions(+), 1005 deletions(-) create mode 100644 backend/api/Controllers/EmergencyActionController.cs create mode 100644 backend/api/EventHandlers/EmergencyActionEventHandler.cs create mode 100644 backend/api/EventHandlers/MissionScheduling.cs create mode 100644 backend/api/Services/EmergencyActionService.cs diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs index a0ef9fe60..62d8eb5c5 100644 --- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs +++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs @@ -25,14 +25,11 @@ public class TestMissionEventHandler : IDisposable { private static readonly Installation testInstallation = new() { - InstallationCode = "test", - Name = "test test" + InstallationCode = "test", Name = "test test" }; private static readonly Plant testPlant = new() { - PlantCode = "test", - Name = "test test", - Installation = testInstallation + PlantCode = "test", Name = "test test", Installation = testInstallation }; private readonly FlotillaDbContext _context; @@ -96,18 +93,14 @@ public TestMissionEventHandler(DatabaseFixture fixture) { Deck = new Deck { - Plant = testPlant, - Installation = testInstallation, - Name = "testDeck" + Plant = testPlant, Installation = testInstallation, Name = "testDeck" }, Installation = testInstallation, Plant = testPlant, Name = "testArea", MapMetadata = new MapMetadata { - MapName = "TestMap", - Boundary = new Boundary(), - TransformationMatrices = new TransformationMatrices() + MapName = "TestMap", Boundary = new Boundary(), TransformationMatrices = new TransformationMatrices() }, DefaultLocalizationPose = new Pose(), SafePositions = new List() @@ -118,14 +111,13 @@ public TestMissionEventHandler(DatabaseFixture fixture) { Name = "testMission", MissionId = Guid.NewGuid().ToString(), + MissionRunPriority = MissionRunPriority.Normal, Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.Now, Area = NewArea, Map = new MapMetadata { - MapName = "TestMap", - Boundary = new Boundary(), - TransformationMatrices = new TransformationMatrices() + MapName = "TestMap", Boundary = new Boundary(), TransformationMatrices = new TransformationMatrices() }, InstallationCode = "testInstallation" }; @@ -327,8 +319,7 @@ private void SetupMocksForRobotController(Robot robot, MissionRun missionRun) new IsarMission( new IsarStartMissionResponse { - MissionId = "test", - Tasks = new List() + MissionId = "test", Tasks = new List() } ) ); diff --git a/backend/api/Controllers/EmergencyActionController.cs b/backend/api/Controllers/EmergencyActionController.cs new file mode 100644 index 000000000..1680b5f55 --- /dev/null +++ b/backend/api/Controllers/EmergencyActionController.cs @@ -0,0 +1,104 @@ +using Api.Controllers.Models; +using Api.Database.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 IAreaService _areaService; + private readonly IEmergencyActionService _emergencyActionService; + private readonly ILogger _logger; + private readonly IRobotService _robotService; + + public EmergencyActionController(ILogger logger, IRobotService robotService, IAreaService areaService, IEmergencyActionService emergencyActionService) + { + _logger = logger; + _robotService = robotService; + _areaService = areaService; + _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("{robotId}/{installationCode}/{areaName}/abort-current-mission-and-go-to-safe-zone")] + [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 AbortCurrentMissionAndGoToSafeZone( + [FromRoute] string robotId, + [FromRoute] string installationCode, + [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"); + } + + var area = await _areaService.ReadByInstallationAndName(installationCode, areaName); + if (area == null) + { + _logger.LogError("Could not find area {AreaName} for installation code {InstallationCode}", areaName, installationCode); + return NotFound("Area not found"); + } + + _emergencyActionService.TriggerEmergencyButtonPressedForRobot(new EmergencyButtonPressedForRobotEventArgs(robot.Id, area.Id)); + + return Ok("Request to abort current mission and move robot back to safe position received"); + } + + /// + /// 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("{robotId}/{installationCode}/{areaName}/clear-emergency-state")] + [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 ClearEmergencyState( + [FromRoute] string robotId, + [FromRoute] string installationCode, + [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"); + } + + var area = await _areaService.ReadByInstallationAndName(installationCode, areaName); + if (area == null) + { + _logger.LogError("Could not find area {AreaName} for installation code {InstallationCode}", areaName, installationCode); + return NotFound("Area not found"); + } + + _emergencyActionService.TriggerEmergencyButtonDepressedForRobot(new EmergencyButtonPressedForRobotEventArgs(robot.Id, area.Id)); + + return Ok("Request to clear emergency state for robot was received"); + } + } +} diff --git a/backend/api/Controllers/MissionController.cs b/backend/api/Controllers/MissionController.cs index 030b62e05..5e7eb561d 100644 --- a/backend/api/Controllers/MissionController.cs +++ b/backend/api/Controllers/MissionController.cs @@ -306,6 +306,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery Name = missionDefinition.Name, Robot = robot, MissionId = missionDefinition.Id, + MissionRunPriority = MissionRunPriority.Normal, Status = MissionStatus.Pending, DesiredStartTime = scheduledMissionQuery.DesiredStartTime, Tasks = missionTasks, @@ -347,6 +348,8 @@ public async Task> Create( [FromBody] ScheduledMissionQuery scheduledMissionQuery ) { + // TODO: once we have a good way of creating mission definitions for echo missions, + // we can delete this endpoint var robot = await _robotService.ReadById(scheduledMissionQuery.RobotId); if (robot is null) { @@ -438,6 +441,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery Name = echoMission.Name, Robot = robot, MissionId = scheduledMissionDefinition.Id, + MissionRunPriority = MissionRunPriority.Normal, Status = MissionStatus.Pending, DesiredStartTime = scheduledMissionQuery.DesiredStartTime, Tasks = missionTasks, @@ -455,7 +459,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery if (existingMissionDefinition == null) { - await _missionDefinitionService.Create(scheduledMissionDefinition); + var newMissionDefinition = await _missionDefinitionService.Create(scheduledMissionDefinition); } var newMissionRun = await _missionRunService.Create(missionRun); @@ -549,6 +553,7 @@ [FromBody] CustomMissionQuery customMissionQuery Name = customMissionQuery.Name, Description = customMissionQuery.Description, MissionId = customMissionDefinition.Id, + MissionRunPriority = MissionRunPriority.Normal, Comment = customMissionQuery.Comment, Robot = robot, Status = MissionStatus.Pending, @@ -568,7 +573,7 @@ [FromBody] CustomMissionQuery customMissionQuery if (existingMissionDefinition == null) { - await _missionDefinitionService.Create(customMissionDefinition); + var newMissionDefinition = await _missionDefinitionService.Create(customMissionDefinition); } var newMissionRun = await _missionRunService.Create(scheduledMission); diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index 9fc8814f6..01f6cdaee 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; + await _robotService.Update(robot); + robot.CurrentArea = area; + 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/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 15a9724e8..110e8eb72 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( @@ -107,7 +112,6 @@ public void UpdateWithIsarInfo(IsarMission isarMission) && task.IsarTaskId.Equals(isarTaskId, StringComparison.Ordinal) ); } - #nullable disable public static MissionStatus MissionStatusFromString(string status) @@ -122,9 +126,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" + ) }; } @@ -136,7 +140,7 @@ public void CalculateEstimatedDuration() task => task.Inspections.Sum(inspection => inspection.VideoDuration ?? 0) ); EstimatedDuration = (uint)( - (Robot.Model.AverageDurationPerTag * Tasks.Count) + totalInspectionDuration + Robot.Model.AverageDurationPerTag * Tasks.Count + totalInspectionDuration ); } else @@ -163,8 +167,8 @@ public void CalculateEstimatedDuration() prevPosition = currentPosition; } int estimate = (int)( - (distance / (RobotVelocity * EfficiencyFactor)) - + (numberOfTags * InspectionTime) + distance / (RobotVelocity * EfficiencyFactor) + + numberOfTags * InspectionTime ); EstimatedDuration = (uint)estimate * 60; } @@ -198,4 +202,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 02871e8ef..bfd289eb8 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -1,12 +1,50 @@ 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 +84,9 @@ public class Robot [Required] public bool Enabled { get; set; } + [Required] + public bool MissionQueueFrozen { get; set; } + [Required] public RobotStatus Status { get; set; } @@ -61,52 +102,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 @@ -114,6 +116,6 @@ public enum RobotStatus Available, Busy, Offline, - Deprecated, + Deprecated } } diff --git a/backend/api/EventHandlers/EmergencyActionEventHandler.cs b/backend/api/EventHandlers/EmergencyActionEventHandler.cs new file mode 100644 index 000000000..0cfd1e7a9 --- /dev/null +++ b/backend/api/EventHandlers/EmergencyActionEventHandler.cs @@ -0,0 +1,37 @@ +using Api.Services; +using Api.Services.Events; +using Api.Utilities; +namespace Api.EventHandlers +{ + public class EmergencyActionEventHandler : EventHandlerBase + { + private readonly ILogger _logger; + + private readonly IServiceScopeFactory _scopeFactory; + + public EmergencyActionEventHandler(ILogger logger, IServiceScopeFactory scopeFactory) + { + _logger = logger; + _scopeFactory = scopeFactory; + + Subscribe(); + } + + public override void Subscribe() + { + EmergencyActionService.EmergencyButtonPressedForRobot += OnEmergencyButtonPressedForRobot; + } + + public override void Unsubscribe() + { + EmergencyActionService.EmergencyButtonPressedForRobot -= OnEmergencyButtonPressedForRobot; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await stoppingToken; + } + + private void OnEmergencyButtonPressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e) { } + } +} diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index fa3fa4ef2..445ebb6c2 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -1,10 +1,8 @@ -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 @@ -31,8 +29,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 MissionScheduling MissionScheduling => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + + private MqttEventHandler MqttEventHandler => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private IList MissionRunQueue(string robotId) { @@ -56,12 +57,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 +86,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); + MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun); _scheduleMissionMutex.ReleaseMutex(); } @@ -102,7 +107,7 @@ 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; @@ -111,102 +116,76 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) var missionRun = MissionRunQueue(robot.Id).First(missionRun => missionRun.Robot.Id == robot.Id); _scheduleMissionMutex.WaitOne(); - StartMissionRunIfSystemIsAvailable(missionRun); + MissionScheduling.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.LogError("Robot with ID: {RobotId} was not found in the database", e.RobotId); + return; + } + + var area = await AreaService.ReadById(e.AreaId); + if (area == null) { - _logger.LogInformation("Mission {MissionRunId} was put on the queue as the system may not start a mission now", missionRun.Id); + _logger.LogError("Could not find area with ID {AreaId}", e.AreaId); return; } + await MissionScheduling.FreezeMissionRunQueueForRobot(robot); + try { - StartMissionRun(missionRun); + await MissionScheduling.StopCurrentMissionRun(robot); } 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(); + await MissionScheduling.ScheduleMissionToReturnToSafePosition(robot, area); } - private async Task TheSystemIsAvailableToRunAMission(Robot robot, MissionRun missionRun) + private async void OnEmergencyButtonDepressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e) { - bool ongoingMission = await OngoingMission(robot.Id); - - 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.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(e.AreaId); + 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}", e.AreaId); } - 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 MissionScheduling.UnfreezeMissionRunQueueForRobot(robot); - private void StartMissionRun(MissionRun queuedMissionRun) - { - var result = RobotController.StartMission( - queuedMissionRun.Robot.Id, - queuedMissionRun.Id - ).Result; - if (result.Result is not OkObjectResult) + if (await MissionScheduling.OngoingMission(robot.Id)) { - string errorMessage = "Unknown error from robot controller"; - if (result.Result is ObjectResult returnObject) - { - errorMessage = returnObject.Value?.ToString() ?? errorMessage; - } - throw new MissionException(errorMessage); + _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); + + MqttEventHandler.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..b1b7d7819 --- /dev/null +++ b/backend/api/EventHandlers/MissionScheduling.cs @@ -0,0 +1,264 @@ +using System.Text.Json; +using Api.Controllers; +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Api.Utilities; +using Microsoft.AspNetCore.Mvc; +namespace Api.EventHandlers +{ + public interface IMissionScheduling + { + public void StartMissionRunIfSystemIsAvailable(MissionRun missionRun); + + public Task TheSystemIsAvailableToRunAMission(Robot robot, MissionRun missionRun); + + public Task OngoingMission(string robotId); + + public void StartMissionRun(MissionRun queuedMissionRun); + } + + public class MissionScheduling : IMissionScheduling + { + private readonly IIsarService _isarService; + private readonly ILogger _logger; + private readonly IMissionRunService _missionRunService; + private readonly RobotController _robotController; + private readonly IRobotService _robotService; + + public MissionScheduling(ILogger logger, IMissionRunService missionRunService, IIsarService isarService, IRobotService robotService, RobotController robotController) + { + _logger = logger; + _missionRunService = missionRunService; + _isarService = isarService; + _robotService = robotService; + _robotController = robotController; + } + + 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 + { + 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(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 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 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 FreezeMissionRunQueueForRobot(Robot robot) + { + 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(Robot robot) + { + 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(Robot robot) + { + if (!await OngoingMission(robot.Id)) + { + _logger.LogWarning("Flotilla has no mission running for robot {RobotName} but an attempt to stop will be made regardless", robot.Name); + } + + 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); + 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); + } + } + + public async Task ScheduleMissionToReturnToSafePosition(Robot robot, Area area) + { + 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 + }; + + 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(); + } + 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) + { + 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 static Pose ClosestSafePosition(Pose robotPose, IList safePositions) + { + if (safePositions == null || !safePositions.Any()) + { + throw new ArgumentException("List of safe positions cannot be null or empty."); + } + + 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/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index 186114c1a..228a99e00 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -68,6 +68,11 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await stoppingToken; } + public void TriggerRobotAvailable(RobotAvailableEventArgs e) + { + OnRobotAvailable(e); + } + protected virtual void OnRobotAvailable(RobotAvailableEventArgs e) { RobotAvailable?.Invoke(this, e); @@ -203,9 +208,7 @@ private static void UpdateVideoStreamsIfChanged(List vid stream => new VideoStream { - Name = stream.Name, - Url = stream.Url, - Type = stream.Type + Name = stream.Name, Url = stream.Url, Type = stream.Type } ) .ToList(); @@ -342,9 +345,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) var missionRunsForEstimation = await missionRunService.ReadAll( new MissionRunQueryStringParameters { - MinDesiredStartTime = minEpochTime, - RobotModelType = robot.Model.Type, - PageSize = QueryStringParameters.MaxPageSize + MinDesiredStartTime = minEpochTime, RobotModelType = robot.Model.Type, PageSize = QueryStringParameters.MaxPageSize } ); var model = robot.Model; diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 359a5c005..9c6bc3fc0 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -49,6 +49,8 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/backend/api/Services/EmergencyActionService.cs b/backend/api/Services/EmergencyActionService.cs new file mode 100644 index 000000000..84be9b41e --- /dev/null +++ b/backend/api/Services/EmergencyActionService.cs @@ -0,0 +1,46 @@ +using Api.Services.Events; +namespace Api.Services +{ + public interface IEmergencyActionService + { + public void TriggerEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobotEventArgs e); + + public void TriggerEmergencyButtonDepressedForRobot(EmergencyButtonPressedForRobotEventArgs e); + } + + public class EmergencyActionService : IEmergencyActionService + { + private readonly ILogger _logger; + private readonly IRobotService _robotService; + + public EmergencyActionService(ILogger logger, IRobotService robotService) + { + _logger = logger; + _robotService = robotService; + } + + public void TriggerEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobotEventArgs e) + { + OnEmergencyButtonPressedForRobot(e); + } + + public void TriggerEmergencyButtonDepressedForRobot(EmergencyButtonPressedForRobotEventArgs e) + { + OnEmergencyButtonPressedForRobot(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..fcf1f9a70 100644 --- a/backend/api/Services/Events/MissionEventArgs.cs +++ b/backend/api/Services/Events/MissionEventArgs.cs @@ -19,4 +19,17 @@ public RobotAvailableEventArgs(string robotId) } public string RobotId { get; set; } } + + public class EmergencyButtonPressedForRobotEventArgs : EventArgs + { + public EmergencyButtonPressedForRobotEventArgs(string robotId, string areaId) + { + RobotId = robotId; + AreaId = areaId; + } + + public string RobotId { get; set; } + + public string AreaId { get; set; } + } } From 6ed28e9b61e7fdaa756f36241d79d0fdfe693339 Mon Sep 17 00:00:00 2001 From: "Mariana R. Santos" Date: Fri, 1 Sep 2023 09:47:22 +0200 Subject: [PATCH 02/11] Add stop all robots backend endpoints --- .../Controllers/EmergencyActionController.cs | 82 ++++++++++++++++++- 1 file changed, 80 insertions(+), 2 deletions(-) diff --git a/backend/api/Controllers/EmergencyActionController.cs b/backend/api/Controllers/EmergencyActionController.cs index 1680b5f55..929227db9 100644 --- a/backend/api/Controllers/EmergencyActionController.cs +++ b/backend/api/Controllers/EmergencyActionController.cs @@ -1,4 +1,5 @@ -using Api.Controllers.Models; +using System.Globalization; +using Api.Controllers.Models; using Api.Database.Models; using Api.Services; using Api.Services.Events; @@ -64,12 +65,47 @@ public async Task AbortCurrentMissionAndGoToSafeZone( return Ok("Request to abort current mission and move robot back to safe position received"); } + /// + /// 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(typeof(MissionRun), StatusCodes.Status200OK)] + [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 + .Where(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, robot.CurrentArea!.Id)); + + } + + return Ok("Request to abort current mission and move all robots back to safe position received"); + } + /// /// 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("{robotId}/{installationCode}/{areaName}/clear-emergency-state")] + [Route("{robotId}/{installationCode}/{areaName}/clear-robot-emergency-state")] [Authorize(Roles = Role.User)] [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -100,5 +136,47 @@ public async Task ClearEmergencyState( return Ok("Request to clear emergency state for robot was received"); } + + /// + /// 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("{robotId}/{installationCode}/{areaName}/clear-emergency-state")] + [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 ClearInstallationEmergencyState( + [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 not found"); + } + + + if (robot.CurrentInstallation == null) + { + _logger.LogWarning("Could not find installation for robot with id {Id}", robotId); + return NotFound("Installation not found"); + } + + if (robot.CurrentArea == null) + { + _logger.LogWarning("Could not find area for robot with id {Id}", robotId); + return NotFound("Area not found"); + } + + _emergencyActionService.TriggerEmergencyButtonDepressedForRobot(new EmergencyButtonPressedForRobotEventArgs(robot.Id, robot.CurrentArea!.Id)); + + return Ok("Request to clear emergency state for robot was received"); + } } } From 87d1c71e193afc8a66c53b7053b7d1291dd63fa1 Mon Sep 17 00:00:00 2001 From: "Mariana R. Santos" Date: Fri, 1 Sep 2023 12:14:58 +0200 Subject: [PATCH 03/11] Add stop all button on frontend --- .../api/EventHandlers/MissionEventHandler.cs | 16 +- frontend/src/api/ApiCaller.tsx | 22 ++ .../Contexts/MissionControlContext.tsx | 2 +- .../MissionOverview/MissionControlButtons.tsx | 2 +- .../MissionOverview/OngoingMissionView.tsx | 16 +- .../FrontPage/MissionOverview/StopDialogs.tsx | 234 ++++++++++++++++++ .../MissionOverview/StopMissionDialog.tsx | 103 -------- frontend/src/language/en.json | 7 +- frontend/src/language/no.json | 7 +- frontend/src/utils/icons.tsx | 3 + 10 files changed, 300 insertions(+), 112 deletions(-) create mode 100644 frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx delete mode 100644 frontend/src/components/Pages/FrontPage/MissionOverview/StopMissionDialog.tsx diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 445ebb6c2..8729d103d 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -113,10 +113,22 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) return; } - var missionRun = MissionRunQueue(robot.Id).First(missionRun => missionRun.Robot.Id == robot.Id); + var missionRun = MissionRunQueue(robot.Id).FirstOrDefault(missionRun => missionRun.Robot.Id == robot.Id); + + if (robot.MissionQueueFrozen == true) + { + missionRun = MissionRunQueue(robot.Id).FirstOrDefault(missionRun => missionRun.Robot.Id == robot.Id && + missionRun.MissionRunPriority == MissionRunPriority.Emergency); + + if (missionRun == null) + { + _logger.LogInformation("The robot was changed to available in emergy state and no Drive to Safe Position mission is scheduled"); + return; + } + } _scheduleMissionMutex.WaitOne(); - MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun); + MissionSchedulingService.StartMissionRunIfSystemIsAvailable(missionRun!); _scheduleMissionMutex.ReleaseMutex(); } diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx index 19da71503..7394ab8da 100644 --- a/frontend/src/api/ApiCaller.tsx +++ b/frontend/src/api/ApiCaller.tsx @@ -472,4 +472,26 @@ export class BackendAPICaller { }) return result.content } + + static async postSafePosition(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 resetRobotState(robotId: string, installationCode: string) { + const path: string = `robots/${robotId}/${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/Pages/FrontPage/MissionOverview/MissionControlButtons.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionControlButtons.tsx index 321b5e837..723016424 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 89bbd9492..dc8a69a5d 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/OngoingMissionView.tsx @@ -10,6 +10,7 @@ import { useNavigate } from 'react-router-dom' import { config } from 'config' import { Icons } from 'utils/icons' import { BackendAPICaller } from 'api/ApiCaller' +import { StopRobotDialog } from './StopDialogs' const StyledOngoingMissionView = styled.div` display: flex; @@ -26,6 +27,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 missionPageSize = 100 @@ -62,9 +69,12 @@ export function OngoingMissionView({ refreshInterval }: RefreshProps) { return ( - - {TranslateText('Ongoing Missions')} - + + + {TranslateText('Ongoing Missions')} + + + {missions.length > 0 && missionDisplay} {missions.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..9b1192e66 --- /dev/null +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx @@ -0,0 +1,234 @@ +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, MissionStatus } from 'models/Mission' +import { useMissionControlContext } from 'components/Contexts/MissionControlContext' +import { BackendAPICaller } from 'api/ApiCaller' +import { Robot, RobotStatus } from 'models/Robot' +import { useInstallationContext } from 'components/Contexts/InstallationContext' + +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 [statusSafePosition, setStatusSafePosition] = useState(false) + const { TranslateText } = useLanguageContext() + const { installationCode } = useInstallationContext() + + const openDialog = async () => { + setIsStopRobotDialogOpen(true) + } + + const closeDialog = async () => { + setIsStopRobotDialogOpen(false) + } + + const stopAll = () => { + BackendAPICaller.postSafePosition(installationCode) + closeDialog() + setStatusSafePosition(true) + return + } + + const resetRobots = () => { + BackendAPICaller.getEnabledRobots().then(async (robots: Robot[]) => { + for (var robot of robots) { + await BackendAPICaller.resetRobotState(robot.id, installationCode) + } + }) + closeDialog() + setStatusSafePosition(false) + } + + return ( + <> + {!statusSafePosition && ( + <> + + + + + {TranslateText('Send robots to safe zone') + '?'} + + + + + + {TranslateText('Send robots to safe zone long text')} + + + {TranslateText('Send robots to safe confirmation text')} + + + + + + + + + + + )} + {statusSafePosition==true && ( + <> + + + + + {TranslateText('Dismiss robots from safe zone') + '?'} + + + + + + {TranslateText('Dismiss robots from safe zone long text')} + + + + + + + + + + + )} + + ) +} \ No newline at end of file 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 fae30961b..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 [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')} - - - - - - - - - - - - ) -} diff --git a/frontend/src/language/en.json b/frontend/src/language/en.json index dae98f934..0295390a0 100644 --- a/frontend/src/language/en.json +++ b/frontend/src/language/en.json @@ -157,5 +157,10 @@ "Echo": "Echo", "Custom": "Custom", "Hours between inspections": "Hours between inspections", - "Days between inspections": "Days between inspections" + "Days between inspections": "Days between inspections", + "Send robots to safe zone": "Send robots to safe zone", + "Send robots to safe zone long text": "You are about to send the robots to a safe zone and thus stop all ongoing missions. To run the missions again, the robots must start from the beginning.", + "Send robots to safe confirmation text": "Are you sure you want to send all the robots to one safe zone?", + "Dismiss robots from safe zone long text": "Normal operation can resume by continuing the missions present on the queue or remove them all.", + "Dismiss robots from safe zone": "Dismiss robots from safe zone" } diff --git a/frontend/src/language/no.json b/frontend/src/language/no.json index 5a2f5679e..877deffcb 100644 --- a/frontend/src/language/no.json +++ b/frontend/src/language/no.json @@ -157,5 +157,10 @@ "Schedule all": "Legg til alle i køen", "No planned inspection": "Ingen inspeksjon planlagt", "Hours between inspections": "Timer mellom inspeksjoner", - "Days between inspections": "Dager mellom inspeksjoner" + "Days between inspections": "Dager mellom inspeksjoner", + "Send robots to safe zone": "Send roboter til trygg sone", + "Send robots to safe zone long text": "Du er i ferd med å sende robotene til en trygg sone og dermed stoppe alle pågående oppdrag. For å kjøre oppdragene igjen må robotene starte fra begynnelsen.", + "Send robots to safe confirmation text": "Er du sikker på at du vil sende alle robotene til en trygg sone?", + "Dismiss robots from safe zone long text": "Vanlig drift kan fortsette ved å starte oppdragene i køen eller ved å slette dem.", + "Dismiss robots from safe zone": "Slepp robotene ut av trygg sone" } diff --git a/frontend/src/utils/icons.tsx b/frontend/src/utils/icons.tsx index ab5b3d3f0..0aab90460 100644 --- a/frontend/src/utils/icons.tsx +++ b/frontend/src/utils/icons.tsx @@ -35,6 +35,7 @@ import { filter_list, navigation, settings, + play, } from '@equinor/eds-icons' Icon.add({ @@ -73,6 +74,7 @@ Icon.add({ filter_list, navigation, settings, + play, }) export enum Icons { @@ -111,4 +113,5 @@ export enum Icons { Filter = 'filter_list', Navigation = 'navigation', Settings = 'settings', + PlayTriangle = 'play', } From af07f4c30b574d35d8daf0beb4f78fa1439d9576 Mon Sep 17 00:00:00 2001 From: "Mariana R. Santos" Date: Mon, 4 Sep 2023 15:49:16 +0200 Subject: [PATCH 04/11] Move entityframework interactions to scoped service --- .../CustomServiceConfigurations.cs | 11 +-- .../Controllers/EmergencyActionController.cs | 3 +- backend/api/Controllers/RobotController.cs | 2 +- .../api/Database/Context/FlotillaDbContext.cs | 2 + backend/api/Database/Context/InitDb.cs | 3 + .../api/EventHandlers/MissionEventHandler.cs | 16 ++--- .../api/EventHandlers/MissionScheduling.cs | 72 ++++++++++++++++--- backend/api/Services/AreaService.cs | 2 +- backend/api/Services/RobotService.cs | 2 +- 9 files changed, 88 insertions(+), 25 deletions(-) diff --git a/backend/api/Configurations/CustomServiceConfigurations.cs b/backend/api/Configurations/CustomServiceConfigurations.cs index 6fda62ea9..6a7638c13 100644 --- a/backend/api/Configurations/CustomServiceConfigurations.cs +++ b/backend/api/Configurations/CustomServiceConfigurations.cs @@ -39,10 +39,13 @@ IConfiguration configuration // Setting splitting behavior explicitly to avoid warning services.AddDbContext( options => - options.UseSqlite( - sqlConnectionString, - o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery) - ) + { + options.UseSqlite( + sqlConnectionString, + o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery) + ); + options.EnableSensitiveDataLogging(); + } ); } else diff --git a/backend/api/Controllers/EmergencyActionController.cs b/backend/api/Controllers/EmergencyActionController.cs index 929227db9..5275c2ccc 100644 --- a/backend/api/Controllers/EmergencyActionController.cs +++ b/backend/api/Controllers/EmergencyActionController.cs @@ -86,8 +86,7 @@ public ActionResult AbortCurrentMissionAndSendAllRobotsToSafeZone( [FromRoute] string installationCode) { - var robots = _robotService.ReadAll().Result - .Where(a => + var robots = _robotService.ReadAll().Result.ToList().FindAll(a => a.CurrentInstallation.ToLower(CultureInfo.CurrentCulture).Equals(installationCode.ToLower(CultureInfo.CurrentCulture), StringComparison.Ordinal) && a.CurrentArea != null); diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index 01f6cdaee..b94c2b747 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -741,8 +741,8 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery robot.Status = RobotStatus.Busy; robot.CurrentMissionId = missionRun.Id; - await _robotService.Update(robot); robot.CurrentArea = area; + await _robotService.Update(robot); return Ok(missionRun); } diff --git a/backend/api/Database/Context/FlotillaDbContext.cs b/backend/api/Database/Context/FlotillaDbContext.cs index 175e8bc43..6f60c52ac 100644 --- a/backend/api/Database/Context/FlotillaDbContext.cs +++ b/backend/api/Database/Context/FlotillaDbContext.cs @@ -65,6 +65,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().OwnsOne(m => m.Map).OwnsOne(t => t.TransformationMatrices); modelBuilder.Entity().OwnsOne(m => m.Map).OwnsOne(b => b.Boundary); + modelBuilder.Entity().HasOne(m => m.Area).WithMany(); + modelBuilder.Entity().HasOne(r => r.CurrentArea).WithMany(); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Orientation); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Position); modelBuilder.Entity().OwnsMany(r => r.VideoStreams); diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index 88dee6c35..b075df0c4 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -134,6 +134,9 @@ private static List GetAreas() MapMetadata = new MapMetadata(), DefaultLocalizationPose = new Pose { }, SafePositions = new List() + { + new() + } }; var area4 = new Area diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 8729d103d..5739e121c 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -31,7 +31,7 @@ IServiceScopeFactory scopeFactory private IAreaService AreaService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private MissionScheduling MissionScheduling => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IMissionScheduling MissionSchedulingService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private MqttEventHandler MqttEventHandler => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); @@ -93,7 +93,7 @@ private void OnMissionRunCreated(object? sender, MissionRunCreatedEventArgs e) } _scheduleMissionMutex.WaitOne(); - MissionScheduling.StartMissionRunIfSystemIsAvailable(missionRun); + MissionSchedulingService.StartMissionRunIfSystemIsAvailable(missionRun); _scheduleMissionMutex.ReleaseMutex(); } @@ -128,7 +128,7 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) } _scheduleMissionMutex.WaitOne(); - MissionSchedulingService.StartMissionRunIfSystemIsAvailable(missionRun!); + MissionSchedulingService.StartMissionRunIfSystemIsAvailable(missionRun); _scheduleMissionMutex.ReleaseMutex(); } @@ -149,11 +149,11 @@ private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyBut return; } - await MissionScheduling.FreezeMissionRunQueueForRobot(robot); + await MissionSchedulingService.FreezeMissionRunQueueForRobot(e.RobotId); try { - await MissionScheduling.StopCurrentMissionRun(robot); + await MissionSchedulingService.StopCurrentMissionRun(e.RobotId); } catch (MissionException ex) { @@ -171,7 +171,7 @@ private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyBut return; } - await MissionScheduling.ScheduleMissionToReturnToSafePosition(robot, area); + await MissionSchedulingService.ScheduleMissionToReturnToSafePosition(e.RobotId, e.AreaId); } private async void OnEmergencyButtonDepressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e) @@ -190,9 +190,9 @@ private async void OnEmergencyButtonDepressedForRobot(object? sender, EmergencyB _logger.LogError("Could not find area with ID {AreaId}", e.AreaId); } - await MissionScheduling.UnfreezeMissionRunQueueForRobot(robot); + await MissionSchedulingService.UnfreezeMissionRunQueueForRobot(e.RobotId); - if (await MissionScheduling.OngoingMission(robot.Id)) + 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); } diff --git a/backend/api/EventHandlers/MissionScheduling.cs b/backend/api/EventHandlers/MissionScheduling.cs index b1b7d7819..ac9352aba 100644 --- a/backend/api/EventHandlers/MissionScheduling.cs +++ b/backend/api/EventHandlers/MissionScheduling.cs @@ -11,11 +11,20 @@ public interface IMissionScheduling { public void StartMissionRunIfSystemIsAvailable(MissionRun missionRun); - public Task TheSystemIsAvailableToRunAMission(Robot robot, MissionRun missionRun); + public Task TheSystemIsAvailableToRunAMission(string robotId, MissionRun missionRun); public Task OngoingMission(string robotId); public void StartMissionRun(MissionRun queuedMissionRun); + + 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 @@ -23,16 +32,18 @@ public class MissionScheduling : IMissionScheduling private readonly IIsarService _isarService; private readonly ILogger _logger; private readonly IMissionRunService _missionRunService; + private readonly IAreaService _areaService; private readonly RobotController _robotController; private readonly IRobotService _robotService; - public MissionScheduling(ILogger logger, IMissionRunService missionRunService, IIsarService isarService, IRobotService robotService, RobotController robotController) + public MissionScheduling(ILogger logger, IMissionRunService missionRunService, IIsarService isarService, IRobotService robotService, RobotController robotController, IAreaService areaService) { _logger = logger; _missionRunService = missionRunService; _isarService = isarService; _robotService = robotService; _robotController = robotController; + _areaService = areaService; } public void StartMissionRunIfSystemIsAvailable(MissionRun missionRun) @@ -62,6 +73,17 @@ public void StartMissionRunIfSystemIsAvailable(MissionRun 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); @@ -130,8 +152,9 @@ public async Task OngoingMission(string robotId) return ongoingMissions.Any(); } - public async Task FreezeMissionRunQueueForRobot(Robot robot) + public async Task FreezeMissionRunQueueForRobot(string robotId) { + var robot = (await _robotService.ReadById(robotId))!; robot.MissionQueueFrozen = true; await _robotService.Update(robot); _logger.LogInformation("Mission queue for robot {RobotName} with ID {RobotId} was frozen", robot.Name, robot.Id); @@ -144,8 +167,25 @@ public async Task UnfreezeMissionRunQueueForRobot(Robot robot) _logger.LogInformation("Mission queue for robot {RobotName} with ID {RobotId} was unfrozen", robot.Name, robot.Id); } - public async Task StopCurrentMissionRun(Robot robot) + 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; + } + await UnfreezeMissionRunQueueForRobot(robot); + } + + 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; + } if (!await OngoingMission(robot.Id)) { _logger.LogWarning("Flotilla has no mission running for robot {RobotName} but an attempt to stop will be made regardless", robot.Name); @@ -154,8 +194,6 @@ public async Task StopCurrentMissionRun(Robot robot) try { await _isarService.StopMission(robot); - robot.CurrentMissionId = null; - await _robotService.Update(robot); } catch (HttpRequestException e) { @@ -176,16 +214,34 @@ public async Task StopCurrentMissionRun(Robot robot) _logger.LogError(e, "{Message}", message); throw new MissionException(message, 0); } + + robot.CurrentMissionId = null; + await _robotService.Update(robot); } - public async Task ScheduleMissionToReturnToSafePosition(Robot robot, Area area) + 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 = 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 + RobotPose = clonedPose, + Inspections = new List(), + InspectionTarget = new Position(), + TaskOrder = 0 }; var missionRun = new MissionRun diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index 3f2acaa80..984e7daf5 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -78,7 +78,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/RobotService.cs b/backend/api/Services/RobotService.cs index d19bc9259..340f6bb75 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -80,7 +80,7 @@ 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.SafePositions); } } } From 52c0c7c4747590eb06318663d1679b8c2ea44b7d Mon Sep 17 00:00:00 2001 From: "Mariana R. Santos" Date: Fri, 8 Sep 2023 09:53:42 +0200 Subject: [PATCH 05/11] Fix dismiss robots from safe zone functionality --- .../api/Controllers/EmergencyActionController.cs | 5 +---- backend/api/EventHandlers/MissionEventHandler.cs | 5 +++-- backend/api/EventHandlers/MqttEventHandler.cs | 16 +++++++++++++--- backend/api/Program.cs | 2 ++ backend/api/Services/EmergencyActionService.cs | 2 +- frontend/src/api/ApiCaller.tsx | 4 ++-- .../FrontPage/MissionOverview/StopDialogs.tsx | 14 ++++++++++++-- 7 files changed, 34 insertions(+), 14 deletions(-) diff --git a/backend/api/Controllers/EmergencyActionController.cs b/backend/api/Controllers/EmergencyActionController.cs index 5275c2ccc..78e88e2f9 100644 --- a/backend/api/Controllers/EmergencyActionController.cs +++ b/backend/api/Controllers/EmergencyActionController.cs @@ -141,7 +141,7 @@ public async Task ClearEmergencyState( /// safe zone. Clearing the emergency state means that mission runs that may be in the robots queue will start." /// [HttpPost] - [Route("{robotId}/{installationCode}/{areaName}/clear-emergency-state")] + [Route("{robotId}/clear-emergency-state")] [Authorize(Roles = Role.User)] [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -159,14 +159,11 @@ public async Task ClearInstallationEmergencyState( _logger.LogWarning("Could not find robot with id {Id}", robotId); return NotFound("Robot not found"); } - - if (robot.CurrentInstallation == null) { _logger.LogWarning("Could not find installation for robot with id {Id}", robotId); return NotFound("Installation not found"); } - if (robot.CurrentArea == null) { _logger.LogWarning("Could not find area for robot with id {Id}", robotId); diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 5739e121c..5440afd17 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -5,6 +5,7 @@ using Api.Utilities; namespace Api.EventHandlers { + public class MissionEventHandler : EventHandlerBase { private readonly ILogger _logger; @@ -33,7 +34,7 @@ IServiceScopeFactory scopeFactory private IMissionScheduling MissionSchedulingService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private MqttEventHandler MqttEventHandler => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IMqttEventHandler MqttEventHandlerService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private IList MissionRunQueue(string robotId) { @@ -197,7 +198,7 @@ private async void OnEmergencyButtonDepressedForRobot(object? sender, EmergencyB _logger.LogInformation("Robot {RobotName} was unfrozen but the mission to return to safe zone will be completed before further missions are started", robot.Id); } - MqttEventHandler.TriggerRobotAvailable(new RobotAvailableEventArgs(robot.Id)); + MqttEventHandlerService.TriggerRobotAvailable(new RobotAvailableEventArgs(robot.Id)); } } } diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index 228a99e00..89b5a7e15 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -14,7 +14,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 IConfiguration _configuration; private readonly ILogger _logger; @@ -208,7 +214,9 @@ private static void UpdateVideoStreamsIfChanged(List vid stream => new VideoStream { - Name = stream.Name, Url = stream.Url, Type = stream.Type + Name = stream.Name, + Url = stream.Url, + Type = stream.Type } ) .ToList(); @@ -345,7 +353,9 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) var missionRunsForEstimation = await missionRunService.ReadAll( new MissionRunQueryStringParameters { - MinDesiredStartTime = minEpochTime, RobotModelType = robot.Model.Type, PageSize = QueryStringParameters.MaxPageSize + MinDesiredStartTime = minEpochTime, + RobotModelType = robot.Model.Type, + PageSize = QueryStringParameters.MaxPageSize } ); var model = robot.Model; diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 9c6bc3fc0..24c5ad4e4 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -64,7 +64,9 @@ builder.Services.AddScoped(); 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/EmergencyActionService.cs b/backend/api/Services/EmergencyActionService.cs index 84be9b41e..e20e7b072 100644 --- a/backend/api/Services/EmergencyActionService.cs +++ b/backend/api/Services/EmergencyActionService.cs @@ -26,7 +26,7 @@ public void TriggerEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobot public void TriggerEmergencyButtonDepressedForRobot(EmergencyButtonPressedForRobotEventArgs e) { - OnEmergencyButtonPressedForRobot(e); + OnEmergencyButtonDepressedForRobot(e); } public static event EventHandler? EmergencyButtonPressedForRobot; diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx index 7394ab8da..81f53bcdd 100644 --- a/frontend/src/api/ApiCaller.tsx +++ b/frontend/src/api/ApiCaller.tsx @@ -484,8 +484,8 @@ export class BackendAPICaller { return result.content } - static async resetRobotState(robotId: string, installationCode: string) { - const path: string = `robots/${robotId}/${installationCode}/clear-emergency-state` + static async resetRobotState(robotId: string) { + const path: string = `emergency-action/${robotId}/clear-emergency-state` const body = {} const result = await this.POST(path, body).catch((e) => { diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx index 9b1192e66..2b3f4b3fb 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx @@ -138,9 +138,19 @@ export const StopRobotDialog = (): JSX.Element => { } const resetRobots = () => { - BackendAPICaller.getEnabledRobots().then(async (robots: Robot[]) => { + BackendAPICaller.getEnabledRobots().then(robots => robots.filter(robots => robots.currentInstallation.toLowerCase() == installationCode.toLowerCase())).then(async (robots: Robot[]) => { + console.log(robots) for (var robot of robots) { - await BackendAPICaller.resetRobotState(robot.id, installationCode) + console.log(robot.name) + + try + { + await BackendAPICaller.resetRobotState(robot.id) + } + catch(e) + { + console.error(`Failed to POST clear emergency state for ${robot.name}: ` + e) + } } }) closeDialog() From a7ecd053409d725edb79f48bb996ee9af23fefb3 Mon Sep 17 00:00:00 2001 From: "Mariana R. Santos" Date: Mon, 11 Sep 2023 16:05:37 +0200 Subject: [PATCH 06/11] Add ongoing mission to queue in emergency state --- .../api/EventHandlers/MissionScheduling.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/backend/api/EventHandlers/MissionScheduling.cs b/backend/api/EventHandlers/MissionScheduling.cs index ac9352aba..2c827d50b 100644 --- a/backend/api/EventHandlers/MissionScheduling.cs +++ b/backend/api/EventHandlers/MissionScheduling.cs @@ -215,6 +215,29 @@ public async Task StopCurrentMissionRun(string robotId) throw new MissionException(message, 0); } + if (robot.CurrentMissionId != null) + { + var missionRun = await _missionRunService.ReadById(robot.CurrentMissionId); + + if (missionRun != null) + { + var mission = new MissionRun + { + Name = missionRun.Name, + Robot = robot, + MissionRunPriority = MissionRunPriority.Normal, + InstallationCode = missionRun.InstallationCode, + Area = missionRun.Area, + Status = MissionStatus.Pending, + DesiredStartTime = DateTimeOffset.UtcNow, + Tasks = missionRun.Tasks, + Map = new MapMetadata() + }; + + await _missionRunService.Create(mission); + } + } + robot.CurrentMissionId = null; await _robotService.Update(robot); } From e9a0b68c1224d0c4f325d81426e4fede18ce8476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Mon, 18 Sep 2023 16:14:06 +0200 Subject: [PATCH 07/11] Fix formatting --- .../EventHandlers/TestMissionEventHandler.cs | 22 +++-- backend/api/Database/Models/MissionRun.cs | 6 +- backend/api/Database/Models/Robot.cs | 4 +- .../EmergencyActionEventHandler.cs | 9 +- .../api/EventHandlers/MissionEventHandler.cs | 8 +- .../api/Services/EmergencyActionService.cs | 6 +- backend/api/Services/RobotService.cs | 6 +- .../FrontPage/MissionOverview/StopDialogs.tsx | 82 ++++++++++--------- 8 files changed, 79 insertions(+), 64 deletions(-) diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs index 62d8eb5c5..d891ea909 100644 --- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs +++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs @@ -25,11 +25,14 @@ public class TestMissionEventHandler : IDisposable { private static readonly Installation testInstallation = new() { - InstallationCode = "test", Name = "test test" + InstallationCode = "test", + Name = "test test" }; private static readonly Plant testPlant = new() { - PlantCode = "test", Name = "test test", Installation = testInstallation + PlantCode = "test", + Name = "test test", + Installation = testInstallation }; private readonly FlotillaDbContext _context; @@ -93,14 +96,18 @@ public TestMissionEventHandler(DatabaseFixture fixture) { Deck = new Deck { - Plant = testPlant, Installation = testInstallation, Name = "testDeck" + Plant = testPlant, + Installation = testInstallation, + Name = "testDeck" }, Installation = testInstallation, Plant = testPlant, Name = "testArea", MapMetadata = new MapMetadata { - MapName = "TestMap", Boundary = new Boundary(), TransformationMatrices = new TransformationMatrices() + MapName = "TestMap", + Boundary = new Boundary(), + TransformationMatrices = new TransformationMatrices() }, DefaultLocalizationPose = new Pose(), SafePositions = new List() @@ -117,7 +124,9 @@ public TestMissionEventHandler(DatabaseFixture fixture) Area = NewArea, Map = new MapMetadata { - MapName = "TestMap", Boundary = new Boundary(), TransformationMatrices = new TransformationMatrices() + MapName = "TestMap", + Boundary = new Boundary(), + TransformationMatrices = new TransformationMatrices() }, InstallationCode = "testInstallation" }; @@ -319,7 +328,8 @@ private void SetupMocksForRobotController(Robot robot, MissionRun missionRun) new IsarMission( new IsarStartMissionResponse { - MissionId = "test", Tasks = new List() + MissionId = "test", + Tasks = new List() } ) ); diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 110e8eb72..0cd457cd0 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -140,7 +140,7 @@ public void CalculateEstimatedDuration() task => task.Inspections.Sum(inspection => inspection.VideoDuration ?? 0) ); EstimatedDuration = (uint)( - Robot.Model.AverageDurationPerTag * Tasks.Count + totalInspectionDuration + (Robot.Model.AverageDurationPerTag * Tasks.Count) + totalInspectionDuration ); } else @@ -167,8 +167,8 @@ public void CalculateEstimatedDuration() prevPosition = currentPosition; } int estimate = (int)( - distance / (RobotVelocity * EfficiencyFactor) - + numberOfTags * InspectionTime + (distance / (RobotVelocity * EfficiencyFactor)) + + (numberOfTags * InspectionTime) ); EstimatedDuration = (uint)estimate * 60; } diff --git a/backend/api/Database/Models/Robot.cs b/backend/api/Database/Models/Robot.cs index bfd289eb8..a2001b36e 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -28,7 +28,9 @@ public Robot(CreateRobotQuery createQuery) { var videoStream = new VideoStream { - Name = videoStreamQuery.Name, Url = videoStreamQuery.Url, Type = videoStreamQuery.Type + Name = videoStreamQuery.Name, + Url = videoStreamQuery.Url, + Type = videoStreamQuery.Type }; videoStreams.Add(videoStream); } diff --git a/backend/api/EventHandlers/EmergencyActionEventHandler.cs b/backend/api/EventHandlers/EmergencyActionEventHandler.cs index 0cfd1e7a9..2b6c95234 100644 --- a/backend/api/EventHandlers/EmergencyActionEventHandler.cs +++ b/backend/api/EventHandlers/EmergencyActionEventHandler.cs @@ -5,15 +5,8 @@ namespace Api.EventHandlers { public class EmergencyActionEventHandler : EventHandlerBase { - private readonly ILogger _logger; - - private readonly IServiceScopeFactory _scopeFactory; - - public EmergencyActionEventHandler(ILogger logger, IServiceScopeFactory scopeFactory) + public EmergencyActionEventHandler() { - _logger = logger; - _scopeFactory = scopeFactory; - Subscribe(); } diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 5440afd17..664cc3a15 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -123,11 +123,17 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) if (missionRun == null) { - _logger.LogInformation("The robot was changed to available in emergy state and no Drive to Safe Position mission is scheduled"); + _logger.LogInformation("The robot was changed to available in emergency state and no Drive to Safe Position mission is scheduled"); return; } } + if (missionRun == null) + { + _logger.LogInformation("The robot was changed to available but no mission is scheduled"); + return; + } + _scheduleMissionMutex.WaitOne(); MissionSchedulingService.StartMissionRunIfSystemIsAvailable(missionRun); _scheduleMissionMutex.ReleaseMutex(); diff --git a/backend/api/Services/EmergencyActionService.cs b/backend/api/Services/EmergencyActionService.cs index e20e7b072..d35cb3856 100644 --- a/backend/api/Services/EmergencyActionService.cs +++ b/backend/api/Services/EmergencyActionService.cs @@ -10,13 +10,9 @@ public interface IEmergencyActionService public class EmergencyActionService : IEmergencyActionService { - private readonly ILogger _logger; - private readonly IRobotService _robotService; - public EmergencyActionService(ILogger logger, IRobotService robotService) + public EmergencyActionService() { - _logger = logger; - _robotService = robotService; } public void TriggerEmergencyButtonPressedForRobot(EmergencyButtonPressedForRobotEventArgs e) diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index 340f6bb75..1a11c6ca8 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -80,7 +80,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).ThenInclude(r => r.SafePositions); + 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/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx index 2b3f4b3fb..c040af8aa 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx @@ -38,8 +38,6 @@ const Square = styled.div` height: 12px; ` - - interface MissionProps { mission: Mission } @@ -138,34 +136,37 @@ export const StopRobotDialog = (): JSX.Element => { } const resetRobots = () => { - BackendAPICaller.getEnabledRobots().then(robots => robots.filter(robots => robots.currentInstallation.toLowerCase() == installationCode.toLowerCase())).then(async (robots: Robot[]) => { - console.log(robots) - for (var robot of robots) { - console.log(robot.name) - - try - { - await BackendAPICaller.resetRobotState(robot.id) + BackendAPICaller.getEnabledRobots() + .then((robots) => + robots.filter((robots) => robots.currentInstallation.toLowerCase() == installationCode.toLowerCase()) + ) + .then(async (robots: Robot[]) => { + console.log(robots) + for (var robot of robots) { + console.log(robot.name) + + try { + await BackendAPICaller.resetRobotState(robot.id) + } catch (e) { + console.error(`Failed to POST clear emergency state for ${robot.name}: ` + e) + } } - catch(e) - { - console.error(`Failed to POST clear emergency state for ${robot.name}: ` + e) - } - } - }) + }) closeDialog() setStatusSafePosition(false) } return ( - <> + <> {!statusSafePosition && ( - <> - - + <> + + + + {TranslateText('Send robots to safe zone') + '?'} @@ -188,7 +189,7 @@ export const StopRobotDialog = (): JSX.Element => { color="danger" onClick={() => { setIsStopRobotDialogOpen(false) - } } + }} > {TranslateText('Cancel')} @@ -197,21 +198,23 @@ export const StopRobotDialog = (): JSX.Element => { - + + )} - {statusSafePosition==true && ( - <> - - + {statusSafePosition == true && ( + <> + + + + - {TranslateText('Dismiss robots from safe zone') + '?'} + + {TranslateText('Dismiss robots from safe zone') + '?'} + @@ -228,7 +231,7 @@ export const StopRobotDialog = (): JSX.Element => { color="danger" onClick={() => { setIsStopRobotDialogOpen(false) - } } + }} > {TranslateText('Cancel')} @@ -237,8 +240,9 @@ export const StopRobotDialog = (): JSX.Element => { - + + )} ) -} \ No newline at end of file +} From cec29529e242ecbdb195f556dcb040d277bb7a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Tue, 19 Sep 2023 14:12:37 +0200 Subject: [PATCH 08/11] Update unit tests with new mocks and URLs --- backend/api.test/EndpointTest.cs | 31 +++++++++---------- .../EventHandlers/TestMissionEventHandler.cs | 16 ++++++++++ backend/api.test/Mocks/RobotControllerMock.cs | 5 +-- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/backend/api.test/EndpointTest.cs b/backend/api.test/EndpointTest.cs index 480a516cb..95ded55fd 100644 --- a/backend/api.test/EndpointTest.cs +++ b/backend/api.test/EndpointTest.cs @@ -558,27 +558,26 @@ 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) - ); + + // The code below does not work since the request above runs in another thread which may not complete before the code below + /* + string filterQuery = $"?Statuses=Ongoing&Statuses=Pending&PageSize=100&OrderBy=DesiredStartTime"; + var currentMissionResponse = await _client.GetAsync("/missions/runs" + filterQuery); + Assert.True(currentMissionResponse.IsSuccessStatusCode); + + var missionRuns = await currentMissionResponse.Content.ReadFromJsonAsync>(_serializerOptions); + Assert.True(missionRuns != null); + Assert.NotEmpty(missionRuns); + var newMission = missionRuns.Find(m => m.Tasks != null && JsonSerializer.Serialize(m.Tasks[0].RobotPose.Position) == + JsonSerializer.Serialize(testPosition)); + Assert.NotNull(newMission); + */ } [Fact] diff --git a/backend/api.test/EventHandlers/TestMissionEventHandler.cs b/backend/api.test/EventHandlers/TestMissionEventHandler.cs index d891ea909..6e4833bb2 100644 --- a/backend/api.test/EventHandlers/TestMissionEventHandler.cs +++ b/backend/api.test/EventHandlers/TestMissionEventHandler.cs @@ -47,6 +47,12 @@ public class TestMissionEventHandler : IDisposable private readonly RobotControllerMock _robotControllerMock; private readonly IRobotModelService _robotModelService; private readonly IRobotService _robotService; + private readonly IMissionScheduling _missionSchedulingService; + private readonly IIsarService _isarServiceMock; + private readonly IInstallationService _installationService; + private readonly IPlantService _plantService; + private readonly IDeckService _deckService; + private readonly IAreaService _areaService; public TestMissionEventHandler(DatabaseFixture fixture) { @@ -54,6 +60,7 @@ 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 configuration = WebApplication.CreateBuilder().Configuration; @@ -64,6 +71,12 @@ public TestMissionEventHandler(DatabaseFixture fixture) _robotService = new RobotService(_context); _robotModelService = new RobotModelService(_context); _robotControllerMock = new RobotControllerMock(); + _isarServiceMock = new MockIsarService(); + _installationService = new InstallationService(_context); + _plantService = new PlantService(_context, _installationService); + _deckService = new DeckService(_context, _installationService, _plantService); + _areaService = new AreaService(_context, _installationService, _plantService, _deckService); + _missionSchedulingService = new MissionScheduling(missionSchedulingLogger, _missionRunService, _isarServiceMock, _robotService, _robotControllerMock.Mock.Object, _areaService); var mockServiceProvider = new Mock(); @@ -74,6 +87,9 @@ public TestMissionEventHandler(DatabaseFixture fixture) mockServiceProvider .Setup(p => p.GetService(typeof(IRobotService))) .Returns(_robotService); + mockServiceProvider + .Setup(p => p.GetService(typeof(IMissionScheduling))) + .Returns(_missionSchedulingService); mockServiceProvider .Setup(p => p.GetService(typeof(RobotController))) .Returns(_robotControllerMock.Mock.Object); 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 From 3543709a4878632462d268f7b8ccc983d87fe74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Chirico=20Indreb=C3=B8?= Date: Tue, 19 Sep 2023 14:36:47 +0200 Subject: [PATCH 09/11] Remove excess comments and code --- .../CustomServiceConfigurations.cs | 11 +++---- backend/api/Controllers/MissionController.cs | 2 -- backend/api/Database/Models/MissionRun.cs | 1 - .../EmergencyActionEventHandler.cs | 30 ------------------- .../api/EventHandlers/MissionEventHandler.cs | 2 +- 5 files changed, 5 insertions(+), 41 deletions(-) delete mode 100644 backend/api/EventHandlers/EmergencyActionEventHandler.cs diff --git a/backend/api/Configurations/CustomServiceConfigurations.cs b/backend/api/Configurations/CustomServiceConfigurations.cs index 6a7638c13..6fda62ea9 100644 --- a/backend/api/Configurations/CustomServiceConfigurations.cs +++ b/backend/api/Configurations/CustomServiceConfigurations.cs @@ -39,13 +39,10 @@ IConfiguration configuration // Setting splitting behavior explicitly to avoid warning services.AddDbContext( options => - { - options.UseSqlite( - sqlConnectionString, - o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery) - ); - options.EnableSensitiveDataLogging(); - } + options.UseSqlite( + sqlConnectionString, + o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery) + ) ); } else diff --git a/backend/api/Controllers/MissionController.cs b/backend/api/Controllers/MissionController.cs index 5e7eb561d..ff7687e94 100644 --- a/backend/api/Controllers/MissionController.cs +++ b/backend/api/Controllers/MissionController.cs @@ -348,8 +348,6 @@ public async Task> Create( [FromBody] ScheduledMissionQuery scheduledMissionQuery ) { - // TODO: once we have a good way of creating mission definitions for echo missions, - // we can delete this endpoint var robot = await _robotService.ReadById(scheduledMissionQuery.RobotId); if (robot is null) { diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 0cd457cd0..e89c75920 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -112,7 +112,6 @@ public void UpdateWithIsarInfo(IsarMission isarMission) && task.IsarTaskId.Equals(isarTaskId, StringComparison.Ordinal) ); } -#nullable disable public static MissionStatus MissionStatusFromString(string status) { diff --git a/backend/api/EventHandlers/EmergencyActionEventHandler.cs b/backend/api/EventHandlers/EmergencyActionEventHandler.cs deleted file mode 100644 index 2b6c95234..000000000 --- a/backend/api/EventHandlers/EmergencyActionEventHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Api.Services; -using Api.Services.Events; -using Api.Utilities; -namespace Api.EventHandlers -{ - public class EmergencyActionEventHandler : EventHandlerBase - { - public EmergencyActionEventHandler() - { - Subscribe(); - } - - public override void Subscribe() - { - EmergencyActionService.EmergencyButtonPressedForRobot += OnEmergencyButtonPressedForRobot; - } - - public override void Unsubscribe() - { - EmergencyActionService.EmergencyButtonPressedForRobot -= OnEmergencyButtonPressedForRobot; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - await stoppingToken; - } - - private void OnEmergencyButtonPressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e) { } - } -} diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs index 664cc3a15..96e723a57 100644 --- a/backend/api/EventHandlers/MissionEventHandler.cs +++ b/backend/api/EventHandlers/MissionEventHandler.cs @@ -123,7 +123,7 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e) if (missionRun == null) { - _logger.LogInformation("The robot was changed to available in emergency state and no Drive to Safe Position mission is scheduled"); + _logger.LogInformation("The robot was changed to available in emergency state and no emergency mission run is scheduled"); return; } } From c39e6286954a30382cf689b0f0dfba340f928b22 Mon Sep 17 00:00:00 2001 From: aestene Date: Tue, 19 Sep 2023 15:35:36 +0200 Subject: [PATCH 10/11] Fix stop all robots button on the frontend --- frontend/src/App.tsx | 39 ++++++++++--------- .../components/Contexts/SafeZoneContext.tsx | 39 +++++++++++++++++++ .../FrontPage/MissionOverview/StopDialogs.tsx | 15 +++---- 3 files changed, 68 insertions(+), 25 deletions(-) create mode 100644 frontend/src/components/Contexts/SafeZoneContext.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1bb5b9b30..ec9efeebc 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 { MissionQueueProvider } from 'components/Contexts/MissionQueueContext' +import { SafeZoneProvider } from 'components/Contexts/SafeZoneContext' function App() { return ( - - - - <> - -
- -
-
- - - - - - -
-
-
+ + + + + <> + +
+ +
+
+ + + + + + +
+
+
+
) } diff --git a/frontend/src/components/Contexts/SafeZoneContext.tsx b/frontend/src/components/Contexts/SafeZoneContext.tsx new file mode 100644 index 000000000..432eccea7 --- /dev/null +++ b/frontend/src/components/Contexts/SafeZoneContext.tsx @@ -0,0 +1,39 @@ +import { createContext, FC, useContext, 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/StopDialogs.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx index c040af8aa..fb4dade5b 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/StopDialogs.tsx @@ -4,11 +4,12 @@ import { useLanguageContext } from 'components/Contexts/LanguageContext' import { Icons } from 'utils/icons' import { useState, useEffect } from 'react' import { tokens } from '@equinor/eds-tokens' -import { Mission, MissionStatus } from 'models/Mission' +import { Mission } from 'models/Mission' import { useMissionControlContext } from 'components/Contexts/MissionControlContext' import { BackendAPICaller } from 'api/ApiCaller' -import { Robot, RobotStatus } from 'models/Robot' +import { Robot } from 'models/Robot' import { useInstallationContext } from 'components/Contexts/InstallationContext' +import { useSafeZoneContext } from 'components/Contexts/SafeZoneContext' const StyledDisplayButtons = styled.div` display: flex; @@ -116,7 +117,7 @@ export const StopMissionDialog = ({ mission }: MissionProps): JSX.Element => { export const StopRobotDialog = (): JSX.Element => { const [isStopRobotDialogOpen, setIsStopRobotDialogOpen] = useState(false) - const [statusSafePosition, setStatusSafePosition] = useState(false) + const { safeZoneStatus, switchSafeZoneStatus } = useSafeZoneContext() const { TranslateText } = useLanguageContext() const { installationCode } = useInstallationContext() @@ -131,7 +132,7 @@ export const StopRobotDialog = (): JSX.Element => { const stopAll = () => { BackendAPICaller.postSafePosition(installationCode) closeDialog() - setStatusSafePosition(true) + switchSafeZoneStatus(true) return } @@ -153,12 +154,12 @@ export const StopRobotDialog = (): JSX.Element => { } }) closeDialog() - setStatusSafePosition(false) + switchSafeZoneStatus(false) } return ( <> - {!statusSafePosition && ( + {!safeZoneStatus && ( <>