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 30, 2025
1 parent faa27d6 commit b0a0159
Show file tree
Hide file tree
Showing 17 changed files with 188 additions and 33 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 @@ -674,5 +676,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
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, // If data is received, stale time is 10 min before making new API call
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
@@ -1,26 +1,9 @@
import { useQuery } from '@tanstack/react-query'
import { BackendAPICaller } from 'api/ApiCaller'
import { Task, TaskStatus } from 'models/Task'
import { useInspectionsContext } from 'components/Contexts/InpectionsContext'
import { Task } from 'models/Task'
import { StyledInspectionImage } from './InspectionStyles'

export 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, // Waits 1 min before retrying, regardless of how many retries
staleTime: 10 * 60 * 1000, // If data is received, stale time is 10 min before making new API call
enabled:
task.status === TaskStatus.Successful &&
task.isarTaskId !== undefined &&
task.inspection.isarInspectionId !== undefined,
})
return data
}

export const GetInspectionImage = ({ task }: { task: Task }) => {
const { data } = fetchImageData(task)
const { fetchImageData } = useInspectionsContext()
const { data } = fetchImageData(task.inspection.isarInspectionId)
return <>{data !== undefined && <StyledInspectionImage src={data} />}</>
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ export const StyledInspectionOverviewDialogView = styled.div`
gap: 8px;
overflow-y: scroll;
`

export const StyledImagesSection = styled.div`
display: flex;
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ import {
StyledInspection,
} from './InspectionStyles'
import { InspectionOverviewDialogView } from './InspectionOverview'
import { fetchImageData } from './InspectionReportUtilities'
import { useState } from 'react'

interface InspectionDialogViewProps {
selectedTask: Task
tasks: Task[]
Expand All @@ -28,8 +26,8 @@ export const InspectionDialogView = ({ selectedTask, tasks }: InspectionDialogVi
const { TranslateText } = useLanguageContext()
const { installationName } = useInstallationContext()
const { switchSelectedInspectionTask } = useInspectionsContext()
const { data } = fetchImageData(selectedTask)

const { fetchImageData } = useInspectionsContext()
const { data } = fetchImageData(selectedTask.inspection.isarInspectionId)
const [switchImageDirection, setSwitchImageDirection] = useState<number>(0)

const closeDialog = () => {
Expand Down
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 b0a0159

Please sign in to comment.