Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add alert for failure to send robot to safe zone #1294

Merged
merged 3 commits into from
Jan 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions backend/api/Controllers/Models/AlertResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Text.Json.Serialization;
namespace Api.Controllers.Models
{
[method: JsonConstructor]
public class AlertResponse(string code, string name, string message, string installationCode, string? robotId)
{
public string AlertCode { get; set; } = code;
public string AlertName { get; set; } = name;
public string AlertMessage { get; set; } = message;
public string InstallationCode { get; set; } = installationCode;
public string? RobotId { get; set; } = robotId;
}
}
2 changes: 1 addition & 1 deletion backend/api/Database/Context/InitDb.cs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ private static List<Area> GetAreas()
Name = "HB",
MapMetadata = new MapMetadata(),
DefaultLocalizationPose = new DefaultLocalizationPose(),
SafePositions = new List<SafePosition>()
SafePositions = new List<SafePosition>(new[] { new SafePosition() })
};

return new List<Area>(new[]
Expand Down
33 changes: 30 additions & 3 deletions backend/api/EventHandlers/MissionEventHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Api.Database.Models;
using Api.Controllers.Models;
using Api.Database.Models;
using Api.Services;
using Api.Services.Events;
using Api.Utilities;
Expand Down Expand Up @@ -38,6 +39,8 @@ IServiceScopeFactory scopeFactory

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

private ISignalRService SignalRService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<ISignalRService>();

public override void Subscribe()
{
MissionRunService.MissionRunCreated += OnMissionRunCreated;
Expand Down Expand Up @@ -134,8 +137,14 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e)
await RobotService.UpdateCurrentArea(robot.Id, null);
return;
}

try { await ReturnToHomeService.ScheduleReturnToHomeMissionRun(robot.Id); }
catch (Exception ex) when (ex is RobotNotFoundException or AreaNotFoundException or DeckNotFoundException or PoseNotFoundException) { await RobotService.UpdateCurrentArea(robot.Id, null); }
catch (Exception ex) when (ex is RobotNotFoundException or AreaNotFoundException or DeckNotFoundException or PoseNotFoundException)
{
ReportFailureToSignalR(robot, $"Failed to send {robot.Name} to a safe zone");
await RobotService.UpdateCurrentArea(robot.Id, null);
return;
}
return;
}

Expand All @@ -155,6 +164,16 @@ private async void OnRobotAvailable(object? sender, RobotAvailableEventArgs e)
_scheduleMissionSemaphore.Release();
}

private void ReportFailureToSignalR(Robot robot, string message)
{
var installation = robot.CurrentInstallation;
if (installation != null)
_ = SignalRService.SendMessageAsync(
"Alert",
installation,
new AlertResponse("safezoneFailure", "Safezone failure", message, installation.InstallationCode, robot.Id));
}

private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyButtonPressedForRobotEventArgs e)
{
_logger.LogInformation("Triggered EmergencyButtonPressed event for robot ID: {RobotId}", e.RobotId);
Expand All @@ -169,6 +188,7 @@ private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyBut
if (area == null)
{
_logger.LogError("Could not find area with ID {AreaId}", robot.CurrentArea!.Id);
ReportFailureToSignalR(robot, $"Robot {robot.Name} was not correctly localised. Could not find area {robot.CurrentArea.Name}");
return;
}

Expand All @@ -179,6 +199,7 @@ private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyBut
catch (SafeZoneException ex)
{
_logger.LogError(ex, "Failed to schedule return to safe zone mission on robot {RobotName} because: {ErrorMessage}", robot.Name, ex.Message);
ReportFailureToSignalR(robot, $"Failed to send {robot.Name} to a safe zone");
try { await MissionScheduling.UnfreezeMissionRunQueueForRobot(e.RobotId); }
catch (RobotNotFoundException) { return; }
}
Expand All @@ -195,12 +216,14 @@ private async void OnEmergencyButtonPressedForRobot(object? sender, EmergencyBut
if (ex.IsarStatusCode != StatusCodes.Status409Conflict)
{
_logger.LogError(ex, "Failed to stop the current mission on robot {RobotName} because: {ErrorMessage}", robot.Name, ex.Message);
ReportFailureToSignalR(robot, $"Failed to stop current mission for robot {robot.Name}");
return;
}
}
catch (Exception ex)
{
const string Message = "Error in ISAR while stopping current mission, cannot drive to safe position";
ReportFailureToSignalR(robot, $"Robot {robot.Name} failed to drive to safe position");
_logger.LogError(ex, "{Message}", Message);
return;
}
Expand All @@ -219,7 +242,11 @@ private async void OnEmergencyButtonDepressedForRobot(object? sender, EmergencyB
}

var area = await AreaService.ReadById(robot.CurrentArea!.Id);
if (area == null) { _logger.LogError("Could not find area with ID {AreaId}", robot.CurrentArea!.Id); }
if (area == null)
{
_logger.LogError("Could not find area with ID {AreaId}", robot.CurrentArea!.Id);
ReportFailureToSignalR(robot, $"Robot {robot.Name} could not be sent from safe zone as it is not correctly localised");
}

try { await MissionScheduling.UnfreezeMissionRunQueueForRobot(e.RobotId); }
catch (RobotNotFoundException) { return; }
Expand Down
54 changes: 34 additions & 20 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,42 @@ import { MissionFilterProvider } from 'components/Contexts/MissionFilterContext'
import { MissionsProvider } from 'components/Contexts/MissionListsContext'
import { SafeZoneProvider } from 'components/Contexts/SafeZoneContext'
import { AlertProvider } from 'components/Contexts/AlertContext'
import { InstallationProvider } from 'components/Contexts/InstallationContext'
import { AuthProvider } from 'components/Contexts/AuthProvider'
import { SignalRProvider } from 'components/Contexts/SignalRContext'
import { RobotProvider } from 'components/Contexts/RobotContext'

const App = () => (
<AlertProvider>
<SafeZoneProvider>
<MissionsProvider>
<LanguageProvider>
<MissionControlProvider>
<UnauthenticatedTemplate>
<div className="sign-in-page">
<AssetSelectionPage></AssetSelectionPage>
</div>
</UnauthenticatedTemplate>
<AuthenticatedTemplate>
<MissionFilterProvider>
<FlotillaSite />
</MissionFilterProvider>
</AuthenticatedTemplate>
</MissionControlProvider>
</LanguageProvider>
</MissionsProvider>
</SafeZoneProvider>
</AlertProvider>
<InstallationProvider>
<AuthProvider>
<SignalRProvider>
<RobotProvider>
<MissionsProvider>
<AlertProvider>
<SafeZoneProvider>
<MissionsProvider>
<LanguageProvider>
<MissionControlProvider>
<UnauthenticatedTemplate>
<div className="sign-in-page">
<AssetSelectionPage></AssetSelectionPage>
</div>
</UnauthenticatedTemplate>
<AuthenticatedTemplate>
<MissionFilterProvider>
<FlotillaSite />
</MissionFilterProvider>
</AuthenticatedTemplate>
</MissionControlProvider>
</LanguageProvider>
</MissionsProvider>
</SafeZoneProvider>
</AlertProvider>
</MissionsProvider>
</RobotProvider>
</SignalRProvider>
</AuthProvider>
</InstallationProvider>
)

export default App
36 changes: 36 additions & 0 deletions frontend/src/components/Alerts/FailedSafeZoneAlertContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Button, Icon, Typography } from '@equinor/eds-core-react'
import styled from 'styled-components'
import { useLanguageContext } from 'components/Contexts/LanguageContext'
import { Icons } from 'utils/icons'
import { tokens } from '@equinor/eds-tokens'

const StyledDiv = styled.div`
align-items: center;
`

const StyledAlertTitle = styled.div`
display: flex;
gap: 0.3em;
align-items: flex-end;
`

const Indent = styled.div`
padding: 0px 9px;
`

export const FailedSafeZoneAlertContent = ({ message }: { message: string }) => {
const { TranslateText } = useLanguageContext()
return (
<StyledDiv>
<StyledAlertTitle>
<Icon name={Icons.Failed} style={{ color: tokens.colors.interactive.danger__resting.rgba }} />
<Typography>{TranslateText('Safe zone failure')}</Typography>
</StyledAlertTitle>
<Indent>
<Button as={Typography} variant="ghost" color="secondary">
{TranslateText(message)}
</Button>
</Indent>
</StyledDiv>
)
}
36 changes: 34 additions & 2 deletions frontend/src/components/Contexts/AlertContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import { FailedMissionAlertContent } from 'components/Alerts/FailedMissionAlert'
import { BackendAPICaller } from 'api/ApiCaller'
import { SignalREventLabels, useSignalRContext } from './SignalRContext'
import { useInstallationContext } from './InstallationContext'
import { Alert } from 'models/Alert'
import { FailedSafeZoneAlertContent } from 'components/Alerts/FailedSafeZoneAlertContent'
import { useRobotContext } from './RobotContext'

type AlertDictionaryType = { [key in AlertType]?: { content: ReactNode | undefined; dismissFunction: () => void } }

export enum AlertType {
MissionFail,
RequestFail,
SafeZoneFail,
}

type AlertDictionaryType = { [key in AlertType]?: { content: ReactNode | undefined; dismissFunction: () => void } }
const alertTypeEnumMap: { [key: string]: AlertType } = {
safezoneFailure: AlertType.SafeZoneFail,
}

interface IAlertContext {
alerts: AlertDictionaryType
Expand All @@ -38,6 +46,7 @@ export const AlertProvider: FC<Props> = ({ children }) => {
const [recentFailedMissions, setRecentFailedMissions] = useState<Mission[]>([])
const { registerEvent, connectionReady } = useSignalRContext()
const { installationCode } = useInstallationContext()
const { enabledRobots } = useRobotContext()

const pageSize: number = 100
// The default amount of minutes in the past for failed missions to generate an alert
Expand Down Expand Up @@ -95,7 +104,7 @@ export const AlertProvider: FC<Props> = ({ children }) => {

// Register a signalR event handler that listens for new failed missions
useEffect(() => {
if (connectionReady)
if (connectionReady) {
registerEvent(SignalREventLabels.missionRunFailed, (username: string, message: string) => {
const newFailedMission: Mission = JSON.parse(message)
const lastDismissTime: Date = getLastDismissalTime()
Expand All @@ -115,8 +124,31 @@ export const AlertProvider: FC<Props> = ({ children }) => {
return [...failedMissions, newFailedMission]
})
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [registerEvent, connectionReady, installationCode])

useEffect(() => {
if (connectionReady) {
registerEvent(SignalREventLabels.alert, (username: string, message: string) => {
const backendAlert: Alert = JSON.parse(message)
const alertType = alertTypeEnumMap[backendAlert.alertCode]

if (backendAlert.robotId !== null) {
const relevantRobots = enabledRobots.filter((r) => r.id === backendAlert.robotId)
if (!relevantRobots) return
const relevantRobot = relevantRobots[0]
if (relevantRobot.currentInstallation.installationCode !== installationCode) return

// Here we could update the robot state manually, but this is best done on the backend
}

setAlert(alertType, <FailedSafeZoneAlertContent message={backendAlert.alertMessage} />)
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [registerEvent, connectionReady, installationCode, enabledRobots])

useEffect(() => {
if (newFailedMissions.length > 0) {
setAlert(AlertType.MissionFail, <FailedMissionAlertContent missions={newFailedMissions} />)
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/Contexts/SignalRContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,5 @@ export enum SignalREventLabels {
robotUpdated = 'Robot updated',
robotDeleted = 'Robot deleted',
inspectionUpdated = 'Inspection updated',
alert = 'Alert',
}
60 changes: 15 additions & 45 deletions frontend/src/components/Pages/FlotillaSite.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,30 @@ import { config } from 'config'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { FrontPage } from './FrontPage/FrontPage'
import { MissionPage } from './MissionPage/MissionPage'
import { InstallationProvider } from 'components/Contexts/InstallationContext'
import { MissionHistoryPage } from './MissionHistoryPage/MissionHistoryPage'
import { RobotPage } from './RobotPage/RobotPage'
import { AuthProvider } from 'components/Contexts/AuthProvider'
import { APIUpdater } from 'components/Contexts/APIUpdater'
import { MissionDefinitionPage } from './MissionDefinitionPage/MissionDefinitionPage'
import { AssetSelectionPage } from './AssetSelectionPage/AssetSelectionPage'
import { SignalRProvider } from 'components/Contexts/SignalRContext'
import { MissionsProvider } from 'components/Contexts/MissionListsContext'
import { RobotProvider } from 'components/Contexts/RobotContext'

export const FlotillaSite = () => {
return (
<>
<InstallationProvider>
<AuthProvider>
<SignalRProvider>
<RobotProvider>
<MissionsProvider>
<APIUpdater>
<BrowserRouter>
<Routes>
<Route
path={`${config.FRONTEND_BASE_ROUTE}/`}
element={<AssetSelectionPage />}
/>
<Route
path={`${config.FRONTEND_BASE_ROUTE}/FrontPage`}
element={<FrontPage />}
/>
<Route
path={`${config.FRONTEND_BASE_ROUTE}/mission/:missionId`}
element={<MissionPage />}
/>
<Route
path={`${config.FRONTEND_BASE_ROUTE}/mission-definition/:missionId`}
element={<MissionDefinitionPage />}
/>
<Route
path={`${config.FRONTEND_BASE_ROUTE}/history`}
element={<MissionHistoryPage />}
/>
<Route
path={`${config.FRONTEND_BASE_ROUTE}/robot/:robotId`}
element={<RobotPage />}
/>
</Routes>
</BrowserRouter>
</APIUpdater>
</MissionsProvider>
</RobotProvider>
</SignalRProvider>
</AuthProvider>
</InstallationProvider>
<APIUpdater>
<BrowserRouter>
<Routes>
<Route path={`${config.FRONTEND_BASE_ROUTE}/`} element={<AssetSelectionPage />} />
<Route path={`${config.FRONTEND_BASE_ROUTE}/FrontPage`} element={<FrontPage />} />
<Route path={`${config.FRONTEND_BASE_ROUTE}/mission/:missionId`} element={<MissionPage />} />
<Route
path={`${config.FRONTEND_BASE_ROUTE}/mission-definition/:missionId`}
element={<MissionDefinitionPage />}
/>
<Route path={`${config.FRONTEND_BASE_ROUTE}/history`} element={<MissionHistoryPage />} />
<Route path={`${config.FRONTEND_BASE_ROUTE}/robot/:robotId`} element={<RobotPage />} />
</Routes>
</BrowserRouter>
</APIUpdater>
</>
)
}
Loading
Loading