Skip to content

Commit

Permalink
Use IDA MQTT message to update inspection view
Browse files Browse the repository at this point in the history
  • Loading branch information
mrica-equinor committed Jan 17, 2025
1 parent 8d869ef commit 006cb40
Show file tree
Hide file tree
Showing 16 changed files with 190 additions and 30 deletions.
35 changes: 35 additions & 0 deletions backend/api/EventHandlers/MqttEventHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ public override void Subscribe()
MqttService.MqttIsarPressureReceived += OnIsarPressureUpdate;
MqttService.MqttIsarPoseReceived += OnIsarPoseUpdate;
MqttService.MqttIsarCloudHealthReceived += OnIsarCloudHealthUpdate;
MqttService.MqttIdaInspectionResultReceived += OnIdaInspectionResultUpdate;
}

public override void Unsubscribe()
Expand All @@ -95,6 +96,7 @@ public override void Unsubscribe()
MqttService.MqttIsarPressureReceived -= OnIsarPressureUpdate;
MqttService.MqttIsarPoseReceived -= OnIsarPoseUpdate;
MqttService.MqttIsarCloudHealthReceived -= OnIsarCloudHealthUpdate;
MqttService.MqttIdaInspectionResultReceived -= OnIdaInspectionResultUpdate;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
Expand Down Expand Up @@ -671,5 +673,38 @@ private async void OnIsarCloudHealthUpdate(object? sender, MqttReceivedArgs mqtt

TeamsMessageService.TriggerTeamsMessageReceived(new TeamsMessageEventArgs(message));
}

private async void OnIdaInspectionResultUpdate(object? sender, MqttReceivedArgs mqttArgs)
{
var inspectionResult = (IdaInspectionResultMessage)mqttArgs.Message;

var inspectionResultMessage = new InspectionResultMessage
{
InspectionId = inspectionResult.InspectionId,
StorageAccount = inspectionResult.StorageAccount,
BlobContainer = inspectionResult.BlobContainer,
BlobName = inspectionResult.BlobName,
};

var installation = await InstallationService.ReadByInstallationCode(
inspectionResult.BlobContainer,
readOnly: true
);

if (installation == null)
{
_logger.LogError(
"Installation with code {Code} not found when processing IDA inspection result update",
inspectionResult.BlobContainer
);
return;
}

_ = SignalRService.SendMessageAsync(
"Inspection Visulization Ready",
installation,
inspectionResultMessage
);
}
}
}
35 changes: 35 additions & 0 deletions backend/api/MQTT/MessageModels/IdaInspectionResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Text.Json.Serialization;

namespace Api.Mqtt.MessageModels
{
#nullable disable
public class IdaInspectionResultMessage : MqttMessage
{
[JsonPropertyName("inspection_id")]
public string InspectionId { get; set; }

[JsonPropertyName("storageAccount")]
public required string StorageAccount { get; set; }

[JsonPropertyName("blobContainer")]
public required string BlobContainer { get; set; }

[JsonPropertyName("blobName")]
public required string BlobName { get; set; }
}

public class InspectionResultMessage
{
[JsonPropertyName("inspectionId")]
public string InspectionId { get; set; }

[JsonPropertyName("storageAccount")]
public required string StorageAccount { get; set; }

[JsonPropertyName("blobContainer")]
public required string BlobContainer { get; set; }

[JsonPropertyName("blobName")]
public required string BlobName { get; set; }
}
}
50 changes: 50 additions & 0 deletions backend/api/MQTT/MqttService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public MqttService(ILogger<MqttService> logger, IConfiguration config)
public static event EventHandler<MqttReceivedArgs>? MqttIsarPressureReceived;
public static event EventHandler<MqttReceivedArgs>? MqttIsarPoseReceived;
public static event EventHandler<MqttReceivedArgs>? MqttIsarCloudHealthReceived;
public static event EventHandler<MqttReceivedArgs>? MqttIdaInspectionResultReceived;

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Expand Down Expand Up @@ -143,6 +144,9 @@ private Task OnMessageReceived(MqttApplicationMessageReceivedEventArgs messageRe
case Type type when type == typeof(IsarCloudHealthMessage):
OnIsarTopicReceived<IsarCloudHealthMessage>(content);
break;
case Type type when type == typeof(IdaInspectionResultMessage):
OnIdaTopicReceived<IdaInspectionResultMessage>(content);
break;
default:
_logger.LogWarning(
"No callback defined for MQTT message type '{type}'",
Expand Down Expand Up @@ -303,5 +307,51 @@ private void OnIsarTopicReceived<T>(string content)
_logger.LogWarning("{msg}", e.Message);
}
}

private void OnIdaTopicReceived<T>(string content)
where T : MqttMessage
{
T? message;

try
{
message = JsonSerializer.Deserialize<T>(content, serializerOptions);
if (message is null)
{
throw new JsonException();
}
}
catch (Exception ex)
when (ex is JsonException or NotSupportedException or ArgumentException)
{
_logger.LogError(
"Could not create '{className}' object from MQTT message json",
typeof(T).Name
);
return;
}

var type = typeof(T);
try
{
var raiseEvent = type switch
{
_ when type == typeof(IdaInspectionResultMessage) =>
MqttIdaInspectionResultReceived,
_ => throw new NotImplementedException(
$"No event defined for message type '{typeof(T).Name}'"
),
};
// Event will be null if there are no subscribers
if (raiseEvent is not null)
{
raiseEvent(this, new MqttReceivedArgs(message));
}
}
catch (NotImplementedException e)
{
_logger.LogWarning("{msg}", e.Message);
}
}
}
}
1 change: 1 addition & 0 deletions backend/api/MQTT/MqttTopics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public static class MqttTopics
{ "isar/+/pressure", typeof(IsarPressureMessage) },
{ "isar/+/pose", typeof(IsarPoseMessage) },
{ "isar/+/cloud_health", typeof(IsarCloudHealthMessage) },
{ "ida/visualization_available", typeof(IdaInspectionResultMessage) },
};

/// <summary>
Expand Down
6 changes: 4 additions & 2 deletions backend/api/Utilities/Exceptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,16 @@ public class RobotNotAvailableException(string message) : Exception(message) { }
public class RobotBusyException(string message) : Exception(message) { }

public class RobotNotInSameInstallationAsMissionException(string message)
: Exception(message) { }
: Exception(message)
{ }

public class PoseNotFoundException(string message) : Exception(message) { }

public class IsarCommunicationException(string message) : Exception(message) { }

public class ReturnToHomeMissionFailedToScheduleException(string message)
: Exception(message) { }
: Exception(message)
{ }

public class RobotCurrentAreaMissingException(string message) : Exception(message) { }

Expand Down
3 changes: 2 additions & 1 deletion backend/api/appsettings.Development.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"isar/+/pressure",
"isar/+/pose",
"isar/+/cloud_health",
"isar/+/media_config"
"isar/+/media_config",
"ida/visualization_available"
],
"MaxRetryAttempts": 5,
"ShouldFailOnMaxRetries": false
Expand Down
3 changes: 2 additions & 1 deletion backend/api/appsettings.Local.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"isar/+/pressure",
"isar/+/pose",
"isar/+/cloud_health",
"isar/+/media_config"
"isar/+/media_config",
"ida/visualization_available"
],
"MaxRetryAttempts": 5,
"ShouldFailOnMaxRetries": false
Expand Down
3 changes: 2 additions & 1 deletion backend/api/appsettings.Production.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"isar/+/pressure",
"isar/+/pose",
"isar/+/cloud_health",
"isar/+/media_config"
"isar/+/media_config",
"ida/visualization_available"
],
"MaxRetryAttempts": 15,
"ShouldFailOnMaxRetries": true
Expand Down
3 changes: 2 additions & 1 deletion backend/api/appsettings.Staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"isar/+/pressure",
"isar/+/pose",
"isar/+/cloud_health",
"isar/+/media_config"
"isar/+/media_config",
"ida/visualization_available"
],
"MaxRetryAttempts": 15,
"ShouldFailOnMaxRetries": true
Expand Down
3 changes: 2 additions & 1 deletion backend/api/appsettings.Test.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"isar/+/pressure",
"isar/+/pose",
"isar/+/cloud_health",
"isar/+/media_config"
"isar/+/media_config",
"ida/visualization_available"
],
"MaxRetryAttempts": 15,
"ShouldFailOnMaxRetries": true
Expand Down
6 changes: 6 additions & 0 deletions broker/mosquitto/config/access_control
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ topic readwrite isar/#
user flotilla
topic read isar/#

user flotilla
topic read ida/#

user analytics
topic read isar/+/inspection_result

user ida
topic read isar/+/inspection_result

user ida
topic write ida/visualization_available
2 changes: 1 addition & 1 deletion frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ if (config.AI_CONNECTION_STRING.length > 0) {
appInsights.trackPageView()
}

const queryClient = new QueryClient()
export const queryClient = new QueryClient()

const App = () => (
<AuthProvider>
Expand Down
37 changes: 36 additions & 1 deletion frontend/src/components/Contexts/InpectionsContext.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { createContext, FC, useContext, useState } from 'react'
import { createContext, FC, useContext, useEffect, useState } from 'react'
import { Task } from 'models/Task'
import { SignalREventLabels, useSignalRContext } from './SignalRContext'
import { IdaInspectionVisualizationReady } from 'models/Inspection'
import { useQuery } from '@tanstack/react-query'
import { BackendAPICaller } from 'api/ApiCaller'
import { queryClient } from '../../App'

interface IInspectionsContext {
selectedInspectionTask: Task | undefined
switchSelectedInspectionTask: (selectedInspectionTask: Task | undefined) => void
fetchImageData: (inspectionId: string) => any
}

interface Props {
Expand All @@ -13,22 +19,51 @@ interface Props {
const defaultInspectionsContext = {
selectedInspectionTask: undefined,
switchSelectedInspectionTask: () => undefined,
fetchImageData: () => undefined,
}

const InspectionsContext = createContext<IInspectionsContext>(defaultInspectionsContext)

export const InspectionsProvider: FC<Props> = ({ children }) => {
const { registerEvent, connectionReady } = useSignalRContext()
const [selectedInspectionTask, setSelectedInspectionTask] = useState<Task>()

useEffect(() => {
if (connectionReady) {
registerEvent(SignalREventLabels.inspectionVisualizationReady, (username: string, message: string) => {
const inspectionVisualizationData: IdaInspectionVisualizationReady = JSON.parse(message)
queryClient.invalidateQueries({
queryKey: ['fetchInspectionData', inspectionVisualizationData.inspectionId],
})
fetchImageData(inspectionVisualizationData.inspectionId)
})
}
}, [registerEvent, connectionReady])

const switchSelectedInspectionTask = (selectedTask: Task | undefined) => {
setSelectedInspectionTask(selectedTask)
}

const fetchImageData = (inspectionId: string) => {
const data = useQuery({
queryKey: ['fetchInspectionData', inspectionId],
queryFn: async () => {
const imageBlob = await BackendAPICaller.getInspection(inspectionId)
return URL.createObjectURL(imageBlob)
},
retry: 1,
staleTime: 10 * 60 * 1000, // I don't want an API call for 10 min after the first time I get data
enabled: inspectionId !== undefined,
})
return data
}

return (
<InspectionsContext.Provider
value={{
selectedInspectionTask,
switchSelectedInspectionTask,
fetchImageData,
}}
>
{children}
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 @@ -123,4 +123,5 @@ export enum SignalREventLabels {
inspectionUpdated = 'Inspection updated',
alert = 'Alert',
mediaStreamConfigReceived = 'Media stream config received',
inspectionVisualizationReady = 'Inspection Visulization Ready',
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ import {
StyledInspectionImage,
StyledSection,
} from './InspectionStyles'
import { BackendAPICaller } from 'api/ApiCaller'
import { useQuery } from '@tanstack/react-query'

interface InspectionDialogViewProps {
task: Task
Expand All @@ -34,7 +32,8 @@ export const InspectionDialogView = ({ task, tasks }: InspectionDialogViewProps)
const { TranslateText } = useLanguageContext()
const { installationName } = useInstallationContext()
const { switchSelectedInspectionTask } = useInspectionsContext()
const { data } = FetchImageData(task)
const { fetchImageData } = useInspectionsContext()
const { data } = fetchImageData(task.inspection.isarInspectionId)

const closeDialog = () => {
switchSelectedInspectionTask(undefined)
Expand Down Expand Up @@ -155,28 +154,12 @@ export const InspectionsViewSection = ({ tasks, dialogView }: InspectionsViewSec
)
}

const FetchImageData = (task: Task) => {
const data = useQuery({
queryKey: ['fetchInspectionData', task.isarTaskId],
queryFn: async () => {
const imageBlob = await BackendAPICaller.getInspection(task.inspection.isarInspectionId)
return URL.createObjectURL(imageBlob)
},
retryDelay: 60 * 1000, // Will always wait 1 min to retry, regardless of how many retries
staleTime: 10 * 60 * 1000, // I don't want an API call for 10 min after the first time I get data
enabled:
task.status === TaskStatus.Successful &&
task.isarTaskId !== undefined &&
task.inspection.isarInspectionId !== undefined,
})
return data
}

interface IGetInspectionImageProps {
task: Task
}

const GetInspectionImage = ({ task }: IGetInspectionImageProps) => {
const { data } = FetchImageData(task)
const { fetchImageData } = useInspectionsContext()
const { data } = fetchImageData(task.inspection.isarInspectionId)
return <>{data !== undefined && <StyledInspectionImage src={data} />}</>
}
7 changes: 7 additions & 0 deletions frontend/src/models/Inspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,10 @@ export enum InspectionType {
ThermalVideo = 'ThermalVideo',
Audio = 'Audio',
}

export interface IdaInspectionVisualizationReady {
inspectionId: string
storageAccount: string
blobContainer: string
blobName: string
}

0 comments on commit 006cb40

Please sign in to comment.