diff --git a/backend/api/Controllers/Models/AlertResponse.cs b/backend/api/Controllers/Models/AlertResponse.cs
new file mode 100644
index 000000000..94c4b3cff
--- /dev/null
+++ b/backend/api/Controllers/Models/AlertResponse.cs
@@ -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;
+ }
+}
diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs
index 2cf8a23ff..afec59495 100644
--- a/backend/api/Database/Context/InitDb.cs
+++ b/backend/api/Database/Context/InitDb.cs
@@ -241,7 +241,7 @@ private static List GetAreas()
Name = "HB",
MapMetadata = new MapMetadata(),
DefaultLocalizationPose = new DefaultLocalizationPose(),
- SafePositions = new List()
+ SafePositions = new List(new[] { new SafePosition() })
};
return new List(new[]
diff --git a/backend/api/EventHandlers/MissionEventHandler.cs b/backend/api/EventHandlers/MissionEventHandler.cs
index b600d0c46..edbfb16fd 100644
--- a/backend/api/EventHandlers/MissionEventHandler.cs
+++ b/backend/api/EventHandlers/MissionEventHandler.cs
@@ -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;
@@ -38,6 +39,8 @@ IServiceScopeFactory scopeFactory
private IMissionSchedulingService MissionScheduling => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService();
+ private ISignalRService SignalRService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService();
+
public override void Subscribe()
{
MissionRunService.MissionRunCreated += OnMissionRunCreated;
@@ -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;
}
@@ -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);
@@ -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;
}
@@ -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; }
}
@@ -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;
}
@@ -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; }
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index a5738507c..5c7bac1ee 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -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 = () => (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
export default App
diff --git a/frontend/src/components/Alerts/FailedSafeZoneAlertContent.tsx b/frontend/src/components/Alerts/FailedSafeZoneAlertContent.tsx
new file mode 100644
index 000000000..04cc18836
--- /dev/null
+++ b/frontend/src/components/Alerts/FailedSafeZoneAlertContent.tsx
@@ -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 (
+
+
+
+ {TranslateText('Safe zone failure')}
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/Contexts/AlertContext.tsx b/frontend/src/components/Contexts/AlertContext.tsx
index d11d5cb63..71d3ca9ac 100644
--- a/frontend/src/components/Contexts/AlertContext.tsx
+++ b/frontend/src/components/Contexts/AlertContext.tsx
@@ -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
@@ -38,6 +46,7 @@ export const AlertProvider: FC = ({ children }) => {
const [recentFailedMissions, setRecentFailedMissions] = useState([])
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
@@ -95,7 +104,7 @@ export const AlertProvider: FC = ({ 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()
@@ -115,8 +124,31 @@ export const AlertProvider: FC = ({ 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, )
+ })
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [registerEvent, connectionReady, installationCode, enabledRobots])
+
useEffect(() => {
if (newFailedMissions.length > 0) {
setAlert(AlertType.MissionFail, )
diff --git a/frontend/src/components/Contexts/SignalRContext.tsx b/frontend/src/components/Contexts/SignalRContext.tsx
index f7fa9c3ee..e49e78787 100644
--- a/frontend/src/components/Contexts/SignalRContext.tsx
+++ b/frontend/src/components/Contexts/SignalRContext.tsx
@@ -110,4 +110,5 @@ export enum SignalREventLabels {
robotUpdated = 'Robot updated',
robotDeleted = 'Robot deleted',
inspectionUpdated = 'Inspection updated',
+ alert = 'Alert',
}
diff --git a/frontend/src/components/Pages/FlotillaSite.tsx b/frontend/src/components/Pages/FlotillaSite.tsx
index 26aa46be0..ca88800c2 100644
--- a/frontend/src/components/Pages/FlotillaSite.tsx
+++ b/frontend/src/components/Pages/FlotillaSite.tsx
@@ -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 (
<>
-
-
-
-
-
-
-
-
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
- }
- />
-
-
-
-
-
-
-
-
+
+
+
+ } />
+ } />
+ } />
+ }
+ />
+ } />
+ } />
+
+
+
>
)
}
diff --git a/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusSection.tsx b/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusSection.tsx
index 17db3c697..dcf0c099f 100644
--- a/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusSection.tsx
+++ b/frontend/src/components/Pages/FrontPage/RobotCards/RobotStatusSection.tsx
@@ -1,6 +1,6 @@
import { Typography } from '@equinor/eds-core-react'
import { Robot } from 'models/Robot'
-import { useEffect, useState } from 'react'
+import { useEffect } from 'react'
import styled from 'styled-components'
import { RobotStatusCard, RobotStatusCardPlaceholder } from './RobotStatusCard'
import { useInstallationContext } from 'components/Contexts/InstallationContext'
@@ -24,38 +24,20 @@ export const RobotStatusSection = () => {
const { installationCode } = useInstallationContext()
const { enabledRobots } = useRobotContext()
const { switchSafeZoneStatus } = useSafeZoneContext()
- const [robots, setRobots] = useState([])
- useEffect(() => {
- const sortRobotsByStatus = (robots: Robot[]): Robot[] => {
- const sortedRobots = robots.sort((robot, robotToCompareWith) =>
- robot.status! > robotToCompareWith.status! ? 1 : -1
- )
- return sortedRobots
- }
- const relevantRobots = sortRobotsByStatus(
- enabledRobots.filter((robot) => {
- return (
- robot.currentInstallation.installationCode.toLocaleLowerCase() ===
- installationCode.toLocaleLowerCase()
- )
- })
+ const relevantRobots = enabledRobots
+ .filter(
+ (robot) =>
+ robot.currentInstallation.installationCode.toLocaleLowerCase() === installationCode.toLocaleLowerCase()
)
- setRobots(relevantRobots)
-
- const missionQueueFozenStatus = relevantRobots
- .map((robot: Robot) => {
- return robot.missionQueueFrozen
- })
- .filter((status) => status === true)
+ .sort((robot, robotToCompareWith) => (robot.status! > robotToCompareWith.status! ? 1 : -1))
- if (missionQueueFozenStatus.length > 0) switchSafeZoneStatus(true)
- else switchSafeZoneStatus(false)
- }, [enabledRobots, installationCode, switchSafeZoneStatus])
+ useEffect(() => {
+ const missionQueueFozenStatus = relevantRobots.some((robot: Robot) => robot.missionQueueFrozen)
+ switchSafeZoneStatus(missionQueueFozenStatus)
+ }, [enabledRobots, installationCode, switchSafeZoneStatus, relevantRobots])
- const getRobotDisplay = () => {
- return robots.map((robot) => )
- }
+ const robotDisplay = relevantRobots.map((robot) => )
return (
@@ -63,8 +45,8 @@ export const RobotStatusSection = () => {
{TranslateText('Robot Status')}
- {robots.length > 0 && getRobotDisplay()}
- {robots.length === 0 && }
+ {relevantRobots.length > 0 && robotDisplay}
+ {relevantRobots.length === 0 && }
)
diff --git a/frontend/src/models/Alert.ts b/frontend/src/models/Alert.ts
new file mode 100644
index 000000000..4ac30f9d6
--- /dev/null
+++ b/frontend/src/models/Alert.ts
@@ -0,0 +1,6 @@
+export interface Alert {
+ alertCode: string
+ alertName: string
+ alertMessage: string
+ robotId?: string
+}