Add camera mismatch banner to dashboard (#1921)

## Description

Detects if a camera mismatch is present in any camera and displays a
banner in the dashboard for better visibility to the user. All detection
occurs in the backend, and is sent to the frontend via use of a mismatch
boolean included in each vision module.

<img width="1235" alt="image"
src="https://github.com/user-attachments/assets/19219a56-c366-4c56-8c4b-cb5a36fe4a04"
/>

Closes #1920

## Meta

Merge checklist:
- [x] Pull Request title is [short, imperative
summary](https://cbea.ms/git-commit/) of proposed changes
- [x] The description documents the _what_ and _why_
- [ ] If this PR changes behavior or adds a feature, user documentation
is updated
- [ ] If this PR touches photon-serde, all messages have been
regenerated and hashes have not changed unexpectedly
- [x] If this PR touches configuration, this is backwards compatible
with settings back to v2024.3.1
- [x] If this PR touches pipeline settings or anything related to data
exchange, the frontend typing is updated
- [ ] If this PR addresses a bug, a regression test for it is added

---------

Co-authored-by: Sam Freund <techguy763@gmail.com>
Co-authored-by: samfreund <samf.236@proton.me>
Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
This commit is contained in:
Alan
2025-10-21 17:53:22 -07:00
committed by GitHub
parent d44480ddad
commit 054ed8b6a1
11 changed files with 299 additions and 78 deletions

View File

@@ -168,64 +168,7 @@ const deleteThisCamera = (cameraName: string) => {
});
};
const camerasMatch = (camera1: PVCameraInfo, camera2: PVCameraInfo) => {
if (camera1.PVUsbCameraInfo && camera2.PVUsbCameraInfo)
return (
camera1.PVUsbCameraInfo.name === camera2.PVUsbCameraInfo.name &&
camera1.PVUsbCameraInfo.vendorId === camera2.PVUsbCameraInfo.vendorId &&
camera1.PVUsbCameraInfo.productId === camera2.PVUsbCameraInfo.productId &&
camera1.PVUsbCameraInfo.uniquePath === camera2.PVUsbCameraInfo.uniquePath
);
else if (camera1.PVCSICameraInfo && camera2.PVCSICameraInfo)
return (
camera1.PVCSICameraInfo.uniquePath === camera2.PVCSICameraInfo.uniquePath &&
camera1.PVCSICameraInfo.baseName === camera2.PVCSICameraInfo.baseName
);
else if (camera1.PVFileCameraInfo && camera2.PVFileCameraInfo)
return (
camera1.PVFileCameraInfo.uniquePath === camera2.PVFileCameraInfo.uniquePath &&
camera1.PVFileCameraInfo.name === camera2.PVFileCameraInfo.name
);
else return false;
};
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
if (!camera) return null;
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
/**
* Find the PVCameraInfo currently occupying the same uniquepath as the the given module
*/
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
if (!info) {
return {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
};
}
return (
useStateStore().vsmState.allConnectedCameras.find(
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
) || {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
}
);
};
const cameraCononected = (uniquePath: string): boolean => {
const cameraConnected = (uniquePath: string): boolean => {
return (
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
);
@@ -252,8 +195,8 @@ const activeVisionModules = computed(() =>
// Display connected cameras first
.sort(
(first, second) =>
(cameraCononected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
(cameraCononected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
(cameraConnected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
(cameraConnected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
)
);
@@ -274,6 +217,45 @@ const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettin
cameraToDelete.value = camera;
};
const yesDeleteMySettingsText = ref("");
/**
* Get the connection-type-specific camera info from the given PVCameraInfo object.
*/
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
if (!camera) return null;
if (camera.PVUsbCameraInfo) {
return camera.PVUsbCameraInfo;
}
if (camera.PVCSICameraInfo) {
return camera.PVCSICameraInfo;
}
if (camera.PVFileCameraInfo) {
return camera.PVFileCameraInfo;
}
return {};
};
/**
* Find the PVCameraInfo currently occupying the same uniquePath as the the given module
*/
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
if (!info) {
return {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
};
}
return (
useStateStore().vsmState.allConnectedCameras.find(
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
) || {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
}
);
};
</script>
<template>
@@ -290,14 +272,11 @@ const yesDeleteMySettingsText = ref("");
>
<v-card color="surface" class="rounded-12">
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
<v-card-subtitle v-if="!cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
<v-card-subtitle v-if="!cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
>
<v-card-subtitle
v-else-if="
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
camerasMatch(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)
"
v-else-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) && !module.mismatch"
>Status: <span class="active-status">Active</span></v-card-subtitle
>
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
@@ -306,7 +285,7 @@ const yesDeleteMySettingsText = ref("");
<tbody>
<tr
v-if="
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
useStateStore().backendResults[module.uniqueName]
"
>
@@ -348,7 +327,7 @@ const yesDeleteMySettingsText = ref("");
</tbody>
</v-table>
<div
v-if="cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
v-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
:id="`stream-container-${index}`"
class="d-flex flex-column justify-center align-center mt-3"
style="height: 250px"
@@ -370,7 +349,7 @@ const yesDeleteMySettingsText = ref("");
@click="
setCameraView(
module.matchedCameraInfo,
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
>
@@ -441,7 +420,7 @@ const yesDeleteMySettingsText = ref("");
</tr>
<tr>
<td>Connected</td>
<td>{{ cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
<td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
</tr>
</tbody>
</v-table>
@@ -456,7 +435,7 @@ const yesDeleteMySettingsText = ref("");
@click="
setCameraView(
module.matchedCameraInfo,
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
>
@@ -562,7 +541,13 @@ const yesDeleteMySettingsText = ref("");
<v-card-text v-if="!viewingCamera[1]">
<PvCameraInfoCard :camera="viewingCamera[0]" />
</v-card-text>
<v-card-text v-else-if="!camerasMatch(getMatchedDevice(viewingCamera[0]), viewingCamera[0])">
<v-card-text
v-else-if="
activeVisionModules.find(
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath === cameraInfoFor(viewingCamera[0]).uniquePath
)?.mismatch
"
>
<v-alert
class="mb-3"
color="buttonActive"