Skip to content

Commit 054ed8b

Browse files
alaninnovatessamfreundmcm001
authored
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 <[email protected]> Co-authored-by: samfreund <[email protected]> Co-authored-by: Matt Morley <[email protected]>
1 parent d44480d commit 054ed8b

File tree

11 files changed

+299
-78
lines changed

11 files changed

+299
-78
lines changed

photon-client/src/stores/settings/CameraSettingsStore.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,8 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
142142
maxWhiteBalanceTemp: d.maxWhiteBalanceTemp,
143143
matchedCameraInfo: d.matchedCameraInfo,
144144
isConnected: d.isConnected,
145-
hasConnected: d.hasConnected
145+
hasConnected: d.hasConnected,
146+
mismatch: d.mismatch
146147
};
147148
return acc;
148149
}, {});

photon-client/src/types/SettingTypes.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ export interface UiCameraConfiguration {
266266
matchedCameraInfo: PVCameraInfo;
267267
isConnected: boolean;
268268
hasConnected: boolean;
269+
mismatch: boolean;
269270
}
270271

271272
export interface CameraSettingsChangeRequest {
@@ -388,7 +389,8 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = {
388389
PVUsbCameraInfo: undefined
389390
},
390391
isConnected: true,
391-
hasConnected: true
392+
hasConnected: true,
393+
mismatch: false
392394
};
393395

394396
export enum CalibrationBoardTypes {

photon-client/src/types/WebsocketDataTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export interface WebsocketCameraSettingsUpdate {
6969
matchedCameraInfo: PVCameraInfo;
7070
isConnected: boolean;
7171
hasConnected: boolean;
72+
mismatch: boolean;
7273
}
7374
export interface WebsocketNTUpdate {
7475
connected: boolean;

photon-client/src/views/CameraMatchingView.vue

Lines changed: 56 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -168,64 +168,7 @@ const deleteThisCamera = (cameraName: string) => {
168168
});
169169
};
170170
171-
const camerasMatch = (camera1: PVCameraInfo, camera2: PVCameraInfo) => {
172-
if (camera1.PVUsbCameraInfo && camera2.PVUsbCameraInfo)
173-
return (
174-
camera1.PVUsbCameraInfo.name === camera2.PVUsbCameraInfo.name &&
175-
camera1.PVUsbCameraInfo.vendorId === camera2.PVUsbCameraInfo.vendorId &&
176-
camera1.PVUsbCameraInfo.productId === camera2.PVUsbCameraInfo.productId &&
177-
camera1.PVUsbCameraInfo.uniquePath === camera2.PVUsbCameraInfo.uniquePath
178-
);
179-
else if (camera1.PVCSICameraInfo && camera2.PVCSICameraInfo)
180-
return (
181-
camera1.PVCSICameraInfo.uniquePath === camera2.PVCSICameraInfo.uniquePath &&
182-
camera1.PVCSICameraInfo.baseName === camera2.PVCSICameraInfo.baseName
183-
);
184-
else if (camera1.PVFileCameraInfo && camera2.PVFileCameraInfo)
185-
return (
186-
camera1.PVFileCameraInfo.uniquePath === camera2.PVFileCameraInfo.uniquePath &&
187-
camera1.PVFileCameraInfo.name === camera2.PVFileCameraInfo.name
188-
);
189-
else return false;
190-
};
191-
192-
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
193-
if (!camera) return null;
194-
if (camera.PVUsbCameraInfo) {
195-
return camera.PVUsbCameraInfo;
196-
}
197-
if (camera.PVCSICameraInfo) {
198-
return camera.PVCSICameraInfo;
199-
}
200-
if (camera.PVFileCameraInfo) {
201-
return camera.PVFileCameraInfo;
202-
}
203-
return {};
204-
};
205-
206-
/**
207-
* Find the PVCameraInfo currently occupying the same uniquepath as the the given module
208-
*/
209-
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
210-
if (!info) {
211-
return {
212-
PVFileCameraInfo: undefined,
213-
PVCSICameraInfo: undefined,
214-
PVUsbCameraInfo: undefined
215-
};
216-
}
217-
return (
218-
useStateStore().vsmState.allConnectedCameras.find(
219-
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
220-
) || {
221-
PVFileCameraInfo: undefined,
222-
PVCSICameraInfo: undefined,
223-
PVUsbCameraInfo: undefined
224-
}
225-
);
226-
};
227-
228-
const cameraCononected = (uniquePath: string): boolean => {
171+
const cameraConnected = (uniquePath: string): boolean => {
229172
return (
230173
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
231174
);
@@ -252,8 +195,8 @@ const activeVisionModules = computed(() =>
252195
// Display connected cameras first
253196
.sort(
254197
(first, second) =>
255-
(cameraCononected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
256-
(cameraCononected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
198+
(cameraConnected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
199+
(cameraConnected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
257200
)
258201
);
259202
@@ -274,6 +217,45 @@ const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettin
274217
cameraToDelete.value = camera;
275218
};
276219
const yesDeleteMySettingsText = ref("");
220+
221+
/**
222+
* Get the connection-type-specific camera info from the given PVCameraInfo object.
223+
*/
224+
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
225+
if (!camera) return null;
226+
if (camera.PVUsbCameraInfo) {
227+
return camera.PVUsbCameraInfo;
228+
}
229+
if (camera.PVCSICameraInfo) {
230+
return camera.PVCSICameraInfo;
231+
}
232+
if (camera.PVFileCameraInfo) {
233+
return camera.PVFileCameraInfo;
234+
}
235+
return {};
236+
};
237+
238+
/**
239+
* Find the PVCameraInfo currently occupying the same uniquePath as the the given module
240+
*/
241+
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
242+
if (!info) {
243+
return {
244+
PVFileCameraInfo: undefined,
245+
PVCSICameraInfo: undefined,
246+
PVUsbCameraInfo: undefined
247+
};
248+
}
249+
return (
250+
useStateStore().vsmState.allConnectedCameras.find(
251+
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
252+
) || {
253+
PVFileCameraInfo: undefined,
254+
PVCSICameraInfo: undefined,
255+
PVUsbCameraInfo: undefined
256+
}
257+
);
258+
};
277259
</script>
278260

279261
<template>
@@ -290,14 +272,11 @@ const yesDeleteMySettingsText = ref("");
290272
>
291273
<v-card color="surface" class="rounded-12">
292274
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
293-
<v-card-subtitle v-if="!cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
275+
<v-card-subtitle v-if="!cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
294276
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
295277
>
296278
<v-card-subtitle
297-
v-else-if="
298-
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
299-
camerasMatch(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)
300-
"
279+
v-else-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) && !module.mismatch"
301280
>Status: <span class="active-status">Active</span></v-card-subtitle
302281
>
303282
<v-card-subtitle v-else>Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
@@ -306,7 +285,7 @@ const yesDeleteMySettingsText = ref("");
306285
<tbody>
307286
<tr
308287
v-if="
309-
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
288+
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
310289
useStateStore().backendResults[module.uniqueName]
311290
"
312291
>
@@ -348,7 +327,7 @@ const yesDeleteMySettingsText = ref("");
348327
</tbody>
349328
</v-table>
350329
<div
351-
v-if="cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
330+
v-if="cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
352331
:id="`stream-container-${index}`"
353332
class="d-flex flex-column justify-center align-center mt-3"
354333
style="height: 250px"
@@ -370,7 +349,7 @@ const yesDeleteMySettingsText = ref("");
370349
@click="
371350
setCameraView(
372351
module.matchedCameraInfo,
373-
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
352+
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
374353
)
375354
"
376355
>
@@ -441,7 +420,7 @@ const yesDeleteMySettingsText = ref("");
441420
</tr>
442421
<tr>
443422
<td>Connected</td>
444-
<td>{{ cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
423+
<td>{{ cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
445424
</tr>
446425
</tbody>
447426
</v-table>
@@ -456,7 +435,7 @@ const yesDeleteMySettingsText = ref("");
456435
@click="
457436
setCameraView(
458437
module.matchedCameraInfo,
459-
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
438+
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
460439
)
461440
"
462441
>
@@ -562,7 +541,13 @@ const yesDeleteMySettingsText = ref("");
562541
<v-card-text v-if="!viewingCamera[1]">
563542
<PvCameraInfoCard :camera="viewingCamera[0]" />
564543
</v-card-text>
565-
<v-card-text v-else-if="!camerasMatch(getMatchedDevice(viewingCamera[0]), viewingCamera[0])">
544+
<v-card-text
545+
v-else-if="
546+
activeVisionModules.find(
547+
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath === cameraInfoFor(viewingCamera[0]).uniquePath
548+
)?.mismatch
549+
"
550+
>
566551
<v-alert
567552
class="mb-3"
568553
color="buttonActive"

photon-client/src/views/DashboardView.vue

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
1010
import { useTheme } from "vuetify";
1111
1212
const theme = useTheme();
13+
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
1314
1415
const cameraViewType = computed<number[]>({
1516
get: (): number[] => {
@@ -54,6 +55,17 @@ const arducamWarningShown = computed<boolean>(() => {
5455
);
5556
});
5657
58+
const cameraMismatchWarningShown = computed<boolean>(() => {
59+
return (
60+
Object.values(useCameraSettingsStore().cameras)
61+
// Ignore placeholder camera
62+
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
63+
.some((camera) => {
64+
return camera.mismatch;
65+
})
66+
);
67+
});
68+
5769
const conflictingHostnameShown = computed<boolean>(() => {
5870
return useSettingsStore().general.conflictingHostname;
5971
});
@@ -104,6 +116,21 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
104116
{{ useSettingsStore().general.conflictingCameras }}!
105117
</span>
106118
</v-alert>
119+
<v-banner
120+
v-if="cameraMismatchWarningShown"
121+
v-model="cameraMismatchWarningShown"
122+
rounded
123+
color="error"
124+
dark
125+
class="mb-3"
126+
icon="mdi-alert-circle-outline"
127+
>
128+
<span
129+
>Camera Mismatch Detected! Visit the <a href="#/cameraConfigs">Camera Matching</a> page for more information.
130+
Note: Camera matching is done by USB port. Ensure cameras are plugged into the same USB ports as when they were
131+
activated.
132+
</span>
133+
</v-banner>
107134
<v-row no-gutters>
108135
<v-col cols="12" class="pb-3 pr-lg-3" lg="8" align-self="stretch">
109136
<CamerasCard v-model="cameraViewType" />

photon-core/src/main/java/org/photonvision/common/dataflow/networktables/NetworkTablesManager.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public class NetworkTablesManager {
7070
// Creating the alert up here since it should be persistent
7171
private final Alert conflictAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
7272

73+
private final Alert mismatchAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
74+
7375
public boolean conflictingHostname = false;
7476
public String conflictingCameras = "";
7577
private String currentMacAddress;
@@ -95,6 +97,7 @@ private NetworkTablesManager() {
9597

9698
// This should start as false, since we don't know if there's a conflict yet
9799
conflictAlert.set(false);
100+
mismatchAlert.set(false);
98101

99102
// Get the UI state in sync with the backend. NT should fire a callback when it
100103
// first connects to the robot
@@ -115,6 +118,14 @@ public static NetworkTablesManager getInstance() {
115118
return INSTANCE;
116119
}
117120

121+
public void setMismatchAlert(boolean on, String message) {
122+
if (mismatchAlert != null) {
123+
mismatchAlert.set(on);
124+
mismatchAlert.setText(message);
125+
SmartDashboard.updateValues();
126+
}
127+
}
128+
118129
private void logNtMessage(NetworkTableEvent event) {
119130
String levelmsg = "DEBUG";
120131
LogLevel pvlevel = LogLevel.DEBUG;

photon-core/src/main/java/org/photonvision/common/dataflow/websocket/UICameraConfiguration.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public class UICameraConfiguration {
5252
public double minWhiteBalanceTemp;
5353
public double maxWhiteBalanceTemp;
5454
public PVCameraInfo matchedCameraInfo;
55+
public boolean mismatch;
5556

5657
// Status for if the underlying device is present and such
5758
public boolean isConnected;

photon-core/src/main/java/org/photonvision/vision/camera/PVCameraInfo.java

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import com.fasterxml.jackson.annotation.JsonTypeName;
2727
import edu.wpi.first.cscore.UsbCameraInfo;
2828
import java.util.Arrays;
29+
import java.util.Objects;
2930

3031
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
3132
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -70,8 +71,15 @@ default String humanReadableName() {
7071

7172
CameraType type();
7273

74+
/**
75+
* Default equals implementation that delegates to the implementing class's equals method. This
76+
* method checks type compatibility first, then delegates to the actual implementation.
77+
*/
7378
default boolean equals(PVCameraInfo other) {
74-
return uniquePath().equals(other.uniquePath());
79+
if (other == null) return false;
80+
if (this.type() != other.type()) return false;
81+
// Delegate to the actual equals(Object) implementation of this instance
82+
return this.equals((Object) other);
7583
}
7684

7785
@JsonTypeName("PVUsbCameraInfo")
@@ -125,7 +133,17 @@ public CameraType type() {
125133
public boolean equals(Object obj) {
126134
if (this == obj) return true;
127135
if (obj == null) return false;
128-
return obj instanceof PVCameraInfo info && equals(info);
136+
if (!(obj instanceof PVUsbCameraInfo info)) return false;
137+
138+
return super.name.equals(info.name)
139+
&& super.vendorId == info.vendorId
140+
&& super.productId == info.productId
141+
&& uniquePath().equals(info.uniquePath());
142+
}
143+
144+
@Override
145+
public int hashCode() {
146+
return Objects.hash(super.name, super.vendorId, super.productId, uniquePath());
129147
}
130148

131149
@Override
@@ -191,7 +209,14 @@ public CameraType type() {
191209
public boolean equals(Object obj) {
192210
if (this == obj) return true;
193211
if (obj == null) return false;
194-
return obj instanceof PVCameraInfo info && equals(info);
212+
if (!(obj instanceof PVCSICameraInfo info)) return false;
213+
214+
return baseName.equals(info.baseName) && path.equals(info.path);
215+
}
216+
217+
@Override
218+
public int hashCode() {
219+
return Objects.hash(baseName, path);
195220
}
196221

197222
@Override
@@ -248,7 +273,14 @@ public CameraType type() {
248273
public boolean equals(Object obj) {
249274
if (this == obj) return true;
250275
if (obj == null) return false;
251-
return obj instanceof PVFileCameraInfo info && equals(info);
276+
if (!(obj instanceof PVFileCameraInfo info)) return false;
277+
278+
return name.equals(info.name) && path.equals(info.path);
279+
}
280+
281+
@Override
282+
public int hashCode() {
283+
return Objects.hash(name, path);
252284
}
253285

254286
@Override

0 commit comments

Comments
 (0)