Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
884472b
Add camera mismatch banner to dashboard
alaninnovates Apr 19, 2025
74b9ab0
Abstract getMatchedDevice more to take in all devices
alaninnovates Apr 19, 2025
0d5a388
Documentation within matching utils
alaninnovates Apr 19, 2025
449bc2a
run prettier
alaninnovates Apr 19, 2025
8f1db75
Add logs to backend on mismatch
alaninnovates Apr 20, 2025
7597dae
format
alaninnovates Apr 20, 2025
e156af8
Working matching logic
alaninnovates Apr 20, 2025
44ac1ce
add docs for duplicated logic
alaninnovates Apr 20, 2025
846c3c5
run spotless
alaninnovates Apr 20, 2025
0897d4f
Move camera mismatch logic
alaninnovates Apr 20, 2025
3fb3378
format
alaninnovates Apr 20, 2025
e8a756b
change logging format
alaninnovates Apr 20, 2025
7577459
add map that stores prior warned
alaninnovates Apr 21, 2025
ecdd849
make copy of camera info & all modules
alaninnovates Apr 21, 2025
4c1e9b8
move .equals logic to PVCameraInfo class
alaninnovates Apr 21, 2025
9a6aaab
Merge branch 'main' into camera-mismatch-banner
alaninnovates Apr 21, 2025
494403e
Fix camera mismatch showing on no cameras
alaninnovates Apr 22, 2025
759a5c4
Merge branch 'main' into camera-mismatch-banner
samfreund Apr 23, 2025
81508b5
fix calls to getMatchedDevice
alaninnovates Apr 25, 2025
ef11318
format; proper condition for match on dashboard
alaninnovates Apr 26, 2025
7d0eb5f
more verbose camera mismatch err message
alaninnovates Apr 26, 2025
00d5738
Merge branch 'main' into camera-mismatch-banner
alaninnovates Apr 26, 2025
f72d357
Merge branch 'main' into camera-mismatch-banner
samfreund Apr 30, 2025
8e1c27f
Merge branch 'main' into camera-mismatch-banner
samfreund Oct 10, 2025
d2ab513
Merge branch 'main' into camera-mismatch-banner
mcm001 Oct 11, 2025
05d4ea4
Merge branch 'main' into camera-mismatch-banner
samfreund Oct 16, 2025
caf0f2c
add NT alert
samfreund Oct 16, 2025
18fa005
fix unrelated diffs
samfreund Oct 16, 2025
3746d2d
rewrite checking logic
samfreund Oct 16, 2025
acb5984
send mismatched cameras to frontend
samfreund Oct 16, 2025
a14b102
Add mismatch value to vision modules
samfreund Oct 19, 2025
acf4bed
update frontend to use mismatch value from backend
samfreund Oct 19, 2025
aa4b38e
typo fix
samfreund Oct 19, 2025
f34f994
clean up functions
samfreund Oct 19, 2025
e75be6b
Merge branch 'main' into camera-mismatch-banner
samfreund Oct 19, 2025
391a964
refactor mismatchCheck into discrete function
samfreund Oct 20, 2025
4beb779
Merge branch 'main' into camera-mismatch-banner
samfreund Oct 20, 2025
3913fa8
add test
samfreund Oct 20, 2025
8e39317
final docs
samfreund Oct 20, 2025
370c984
resolve comments
samfreund Oct 22, 2025
1058ad3
Merge branch 'main' into camera-mismatch-banner
samfreund Oct 22, 2025
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
68 changes: 68 additions & 0 deletions photon-client/src/lib/MatchingUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { PVCameraInfo, type PVCSICameraInfo, type PVFileCameraInfo, type PVUsbCameraInfo } from "@/types/SettingTypes";

/**
* Check if two cameras match by comparing properties.
* For USB cameras, it checks the name, vendorId, productId, and uniquePath.
* For CSI cameras, it checks the uniquePath and baseName.
* For file cameras, it checks the uniquePath and name.
* Note: When changing this function, change the equivalent function within photon-core's VisionSourceManager class
*/
export 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;
};

/**
* Get the connection-type-specific camera info from the given PVCameraInfo object.
*/
export 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
*/
export const getMatchedDevice = (allDevices: PVCameraInfo[], info: PVCameraInfo | undefined): PVCameraInfo => {
if (!info) {
return {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
};
}
return (
allDevices.find((it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath) || {
PVFileCameraInfo: undefined,
PVCSICameraInfo: undefined,
PVUsbCameraInfo: undefined
}
);
};
90 changes: 21 additions & 69 deletions photon-client/src/views/CameraMatchingView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,13 @@
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { computed, inject, ref } from "vue";
import { useStateStore } from "@/stores/StateStore";
import {
PlaceholderCameraSettings,
PVCameraInfo,
type PVCSICameraInfo,
type PVFileCameraInfo,
type PVUsbCameraInfo,
type UiCameraConfiguration
} from "@/types/SettingTypes";
import { PlaceholderCameraSettings, PVCameraInfo, type UiCameraConfiguration } from "@/types/SettingTypes";
import { getResolutionString } from "@/lib/PhotonUtils";
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
import axios from "axios";
import PvCameraMatchCard from "@/components/common/pv-camera-match-card.vue";
import type { WebsocketCameraSettingsUpdate } from "@/types/WebsocketDataTypes";
import { camerasMatch, cameraInfoFor, getMatchedDevice } from "@/lib/MatchingUtils";

const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
const host = inject<string>("backendHost");
Expand Down Expand Up @@ -95,63 +89,6 @@ 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 => {
return (
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
Expand Down Expand Up @@ -226,7 +163,10 @@ const openExportSettingsPrompt = () => {
<v-card-subtitle
v-else-if="
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
camerasMatch(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)
camerasMatch(
getMatchedDevice(useStateStore().vsmState.allConnectedCameras, module.matchedCameraInfo),
module.matchedCameraInfo
)
"
class="pb-2"
>Status: <span class="active-status">Active</span></v-card-subtitle
Expand Down Expand Up @@ -458,15 +398,27 @@ const openExportSettingsPrompt = () => {
<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="
!camerasMatch(
getMatchedDevice(useStateStore().vsmState.allConnectedCameras, viewingCamera[0]),
viewingCamera[0]
)
"
>
<v-banner rounded color="error" text-color="white" icon="mdi-information-outline" class="mb-3">
It looks like a different camera may have been connected to this device! Compare the following information
carefully.
</v-banner>
<PvCameraMatchCard :saved="viewingCamera[0]" :current="getMatchedDevice(viewingCamera[0])" />
<PvCameraMatchCard
:saved="viewingCamera[0]"
:current="getMatchedDevice(useStateStore().vsmState.allConnectedCameras, viewingCamera[0])"
/>
</v-card-text>
<v-card-text v-else>
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera[0])" />
<PvCameraInfoCard
:camera="getMatchedDevice(useStateStore().vsmState.allConnectedCameras, viewingCamera[0])"
/>
</v-card-text>
</v-card>
</v-dialog>
Expand Down
28 changes: 28 additions & 0 deletions photon-client/src/views/DashboardView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import PipelineConfigCard from "@/components/dashboard/ConfigOptions.vue";
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
import { useStateStore } from "@/stores/StateStore";
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
import { camerasMatch, getMatchedDevice } from "@/lib/MatchingUtils";

const cameraViewType = computed<number[]>({
get: (): number[] => {
Expand Down Expand Up @@ -58,6 +59,18 @@ const arducamWarningShown = computed<boolean>(() => {
)
);
});

const cameraMismatchWarningShown = computed<boolean>(() => {
return Object.values(useCameraSettingsStore().cameras).some(
(camera) =>
camera.nickname !== "Placeholder Camera" &&
camera.isConnected &&
!camerasMatch(
getMatchedDevice(useStateStore().vsmState.allConnectedCameras, camera.matchedCameraInfo),
camera.matchedCameraInfo
)
);
});
</script>

<template>
Expand All @@ -75,6 +88,21 @@ const arducamWarningShown = computed<boolean>(() => {
>Arducam Camera Detected! Please configure the camera model in the <a href="#/cameras">Cameras tab</a>!
</span>
</v-banner>
<v-banner
v-if="cameraMismatchWarningShown"
v-model="cameraMismatchWarningShown"
rounded
color="error"
dark
class="mb-3"
icon="mdi-alert-circle-outline"
>
<span
>Camera Mismatch Detected! Visit the <a href="#/cameraConfigs">Camera Matching</a> page for more information.
Note: Camera matching is done by USB port. Ensure cameras are plugged into the same USB ports as when they were
activated.
</span>
</v-banner>
<v-row no-gutters align="center" justify="center">
<v-col cols="12" class="pb-3 pr-lg-3" lg="8" align-self="stretch">
<CamerasCard v-model="cameraViewType" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,16 @@ public CameraType type() {
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
return obj instanceof PVCameraInfo info && equals(info);
if (obj instanceof PVUsbCameraInfo info) {
return super.name == info.name
&& super.vendorId == info.vendorId
&& super.productId == info.productId
&& equals(info);
}
if (obj instanceof PVCameraInfo info) {
return equals(info);
}
return false;
}

@Override
Expand Down Expand Up @@ -189,7 +198,13 @@ public CameraType type() {
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
return obj instanceof PVCameraInfo info && equals(info);
if (obj instanceof PVCSICameraInfo info) {
return baseName.equals(info.baseName) && equals(info);
}
if (obj instanceof PVCameraInfo info) {
return equals(info);
}
return false;
}

@Override
Expand Down Expand Up @@ -246,7 +261,10 @@ public CameraType type() {
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
return obj instanceof PVFileCameraInfo info && equals(info);
if (obj instanceof PVFileCameraInfo info) {
return name.equals(info.name) && equals(info);
}
return false;
}

@Override
Expand Down
Loading
Loading