mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-24 01:31:44 +00:00
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:
@@ -142,7 +142,8 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
maxWhiteBalanceTemp: d.maxWhiteBalanceTemp,
|
||||
matchedCameraInfo: d.matchedCameraInfo,
|
||||
isConnected: d.isConnected,
|
||||
hasConnected: d.hasConnected
|
||||
hasConnected: d.hasConnected,
|
||||
mismatch: d.mismatch
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
@@ -266,6 +266,7 @@ export interface UiCameraConfiguration {
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
mismatch: boolean;
|
||||
}
|
||||
|
||||
export interface CameraSettingsChangeRequest {
|
||||
@@ -388,7 +389,8 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = {
|
||||
PVUsbCameraInfo: undefined
|
||||
},
|
||||
isConnected: true,
|
||||
hasConnected: true
|
||||
hasConnected: true,
|
||||
mismatch: false
|
||||
};
|
||||
|
||||
export enum CalibrationBoardTypes {
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface WebsocketCameraSettingsUpdate {
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
mismatch: boolean;
|
||||
}
|
||||
export interface WebsocketNTUpdate {
|
||||
connected: boolean;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
const theme = useTheme();
|
||||
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
|
||||
|
||||
const cameraViewType = computed<number[]>({
|
||||
get: (): number[] => {
|
||||
@@ -54,6 +55,17 @@ const arducamWarningShown = computed<boolean>(() => {
|
||||
);
|
||||
});
|
||||
|
||||
const cameraMismatchWarningShown = computed<boolean>(() => {
|
||||
return (
|
||||
Object.values(useCameraSettingsStore().cameras)
|
||||
// Ignore placeholder camera
|
||||
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
|
||||
.some((camera) => {
|
||||
return camera.mismatch;
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
const conflictingHostnameShown = computed<boolean>(() => {
|
||||
return useSettingsStore().general.conflictingHostname;
|
||||
});
|
||||
@@ -104,6 +116,21 @@ const showCameraSetupDialog = ref(useCameraSettingsStore().needsCameraConfigurat
|
||||
{{ useSettingsStore().general.conflictingCameras }}!
|
||||
</span>
|
||||
</v-alert>
|
||||
<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>
|
||||
<v-col cols="12" class="pb-3 pr-lg-3" lg="8" align-self="stretch">
|
||||
<CamerasCard v-model="cameraViewType" />
|
||||
|
||||
@@ -70,6 +70,8 @@ public class NetworkTablesManager {
|
||||
// Creating the alert up here since it should be persistent
|
||||
private final Alert conflictAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
|
||||
|
||||
private final Alert mismatchAlert = new Alert("PhotonAlerts", "", AlertType.kWarning);
|
||||
|
||||
public boolean conflictingHostname = false;
|
||||
public String conflictingCameras = "";
|
||||
private String currentMacAddress;
|
||||
@@ -95,6 +97,7 @@ public class NetworkTablesManager {
|
||||
|
||||
// This should start as false, since we don't know if there's a conflict yet
|
||||
conflictAlert.set(false);
|
||||
mismatchAlert.set(false);
|
||||
|
||||
// Get the UI state in sync with the backend. NT should fire a callback when it
|
||||
// first connects to the robot
|
||||
@@ -115,6 +118,14 @@ public class NetworkTablesManager {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void setMismatchAlert(boolean on, String message) {
|
||||
if (mismatchAlert != null) {
|
||||
mismatchAlert.set(on);
|
||||
mismatchAlert.setText(message);
|
||||
SmartDashboard.updateValues();
|
||||
}
|
||||
}
|
||||
|
||||
private void logNtMessage(NetworkTableEvent event) {
|
||||
String levelmsg = "DEBUG";
|
||||
LogLevel pvlevel = LogLevel.DEBUG;
|
||||
|
||||
@@ -52,6 +52,7 @@ public class UICameraConfiguration {
|
||||
public double minWhiteBalanceTemp;
|
||||
public double maxWhiteBalanceTemp;
|
||||
public PVCameraInfo matchedCameraInfo;
|
||||
public boolean mismatch;
|
||||
|
||||
// Status for if the underlying device is present and such
|
||||
public boolean isConnected;
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -70,8 +71,15 @@ public sealed interface PVCameraInfo {
|
||||
|
||||
CameraType type();
|
||||
|
||||
/**
|
||||
* Default equals implementation that delegates to the implementing class's equals method. This
|
||||
* method checks type compatibility first, then delegates to the actual implementation.
|
||||
*/
|
||||
default boolean equals(PVCameraInfo other) {
|
||||
return uniquePath().equals(other.uniquePath());
|
||||
if (other == null) return false;
|
||||
if (this.type() != other.type()) return false;
|
||||
// Delegate to the actual equals(Object) implementation of this instance
|
||||
return this.equals((Object) other);
|
||||
}
|
||||
|
||||
@JsonTypeName("PVUsbCameraInfo")
|
||||
@@ -125,7 +133,17 @@ public sealed interface PVCameraInfo {
|
||||
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 false;
|
||||
|
||||
return super.name.equals(info.name)
|
||||
&& super.vendorId == info.vendorId
|
||||
&& super.productId == info.productId
|
||||
&& uniquePath().equals(info.uniquePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(super.name, super.vendorId, super.productId, uniquePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -191,7 +209,14 @@ public sealed interface PVCameraInfo {
|
||||
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 false;
|
||||
|
||||
return baseName.equals(info.baseName) && path.equals(info.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(baseName, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -248,7 +273,14 @@ public sealed interface PVCameraInfo {
|
||||
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 false;
|
||||
|
||||
return name.equals(info.name) && path.equals(info.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -93,6 +93,8 @@ public class VisionModule {
|
||||
MJPGFrameConsumer inputVideoStreamer;
|
||||
MJPGFrameConsumer outputVideoStreamer;
|
||||
|
||||
boolean mismatch;
|
||||
|
||||
public VisionModule(PipelineManager pipelineManager, VisionSource visionSource) {
|
||||
logger =
|
||||
new Logger(
|
||||
@@ -100,6 +102,8 @@ public class VisionModule {
|
||||
visionSource.getSettables().getConfiguration().nickname,
|
||||
LogGroup.VisionModule);
|
||||
|
||||
mismatch = false;
|
||||
|
||||
cameraQuirks = visionSource.getCameraConfiguration().cameraQuirks;
|
||||
|
||||
if (visionSource.getCameraConfiguration().cameraQuirks == null)
|
||||
@@ -568,6 +572,8 @@ public class VisionModule {
|
||||
|
||||
ret.deactivated = config.deactivated;
|
||||
|
||||
ret.mismatch = this.mismatch;
|
||||
|
||||
// TODO refactor into helper method
|
||||
var temp = new HashMap<Integer, HashMap<String, Object>>();
|
||||
var videoModes = visionSource.getSettables().getAllVideoModes();
|
||||
|
||||
@@ -31,6 +31,7 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.networktables.NetworkTablesManager;
|
||||
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
@@ -311,16 +312,118 @@ public class VisionSourceManager {
|
||||
.forEach(cameraInfos::add);
|
||||
}
|
||||
|
||||
// FileVisionSources are a bit quirky. They aren't enumerated by the above, but i still want my
|
||||
// FileVisionSources are a bit quirky. They aren't enumerated by the above, but I still want my
|
||||
// UI to look like it ought to work
|
||||
vmm.getModules().stream()
|
||||
.map(it -> it.getCameraConfiguration().matchedCameraInfo)
|
||||
.filter(info -> info instanceof PVCameraInfo.PVFileCameraInfo)
|
||||
.forEach(cameraInfos::add);
|
||||
|
||||
checkMismatches(cameraInfos);
|
||||
|
||||
return cameraInfos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for mismatches between connected cameras and saved camera configurations.
|
||||
*
|
||||
* <p>Note that if the information for a camera spontaneously changes without it being
|
||||
* disconnected/unplugged and reconnected/replugged, we may experience unexpected behavior.
|
||||
*
|
||||
* @param cameraInfos List of currently connected camera infos, checked against saved configs
|
||||
*/
|
||||
protected void checkMismatches(List<PVCameraInfo> cameraInfos) {
|
||||
// from the listed physical camera infos, match them to the camera configs and check for
|
||||
// mismatches
|
||||
for (VisionModule module : vmm.getModules()) {
|
||||
PVCameraInfo matchedCameraInfo = module.getCameraConfiguration().matchedCameraInfo;
|
||||
// We use unique paths to determine if the module has a camera in the port. If no unique path
|
||||
// is found that matches the module, it's removed from the mismatched set as a disconnected
|
||||
// camera cannot be mismatched.
|
||||
if (!cameraInfos.stream()
|
||||
.map(PVCameraInfo::uniquePath)
|
||||
.toList()
|
||||
.contains(matchedCameraInfo.uniquePath())) {
|
||||
module.mismatch = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (PVCameraInfo info : cameraInfos) {
|
||||
// if the unique path doesn't match, skip cause it's not in the same port
|
||||
if (!matchedCameraInfo.uniquePath().equals(info.uniquePath())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the camera info doesn't match, log an error
|
||||
if (!matchedCameraInfo.equals(info) && !module.mismatch) {
|
||||
logger.error("Camera mismatch error!");
|
||||
logger.error("Camera config mismatch for " + matchedCameraInfo.name());
|
||||
logCameraInfoDiff(matchedCameraInfo, info);
|
||||
module.mismatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the NetworkTables mismatch alert
|
||||
if (vmm.getModules().stream().anyMatch(m -> m.mismatch)) {
|
||||
NetworkTablesManager.getInstance()
|
||||
.setMismatchAlert(
|
||||
true,
|
||||
"Camera mismatch error! See logs for details. ("
|
||||
+ vmm.getModules().stream()
|
||||
.filter(m -> m.mismatch)
|
||||
.map(m -> m.getCameraConfiguration().nickname)
|
||||
.toList()
|
||||
.toString()
|
||||
.replaceAll("[\\[\\]()]", "")
|
||||
+ " affected)");
|
||||
} else {
|
||||
NetworkTablesManager.getInstance().setMismatchAlert(false, "");
|
||||
}
|
||||
}
|
||||
|
||||
/** Log the differences between two PVCameraInfo objects. */
|
||||
private static void logCameraInfoDiff(PVCameraInfo saved, PVCameraInfo current) {
|
||||
String expected = "Expected: Name: " + saved.name();
|
||||
String actual = "Actual: Name: " + current.name();
|
||||
if (saved instanceof PVCameraInfo.PVCSICameraInfo savedCsi
|
||||
&& current instanceof PVCameraInfo.PVCSICameraInfo currentCsi) {
|
||||
expected += " Base Name: " + savedCsi.baseName;
|
||||
actual += " Base Name: " + currentCsi.baseName;
|
||||
}
|
||||
|
||||
expected += " Type: " + saved.type().toString();
|
||||
actual += " Type: " + current.type().toString();
|
||||
|
||||
if (saved instanceof PVCameraInfo.PVUsbCameraInfo savedUsb
|
||||
&& current instanceof PVCameraInfo.PVUsbCameraInfo currentUsb) {
|
||||
expected +=
|
||||
" Device Number: "
|
||||
+ savedUsb.dev
|
||||
+ " Vendor ID: "
|
||||
+ savedUsb.vendorId
|
||||
+ " Product ID: "
|
||||
+ savedUsb.productId;
|
||||
actual +=
|
||||
" Device Number: "
|
||||
+ currentUsb.dev
|
||||
+ " Vendor ID: "
|
||||
+ currentUsb.vendorId
|
||||
+ " Product ID: "
|
||||
+ currentUsb.productId;
|
||||
}
|
||||
|
||||
expected += " Path: " + saved.path();
|
||||
actual += " Path: " + current.path();
|
||||
expected += " Unique Path: " + saved.uniquePath();
|
||||
actual += " Unique Path: " + current.uniquePath();
|
||||
expected += " Other Paths: " + Arrays.toString(saved.otherPaths());
|
||||
actual += " Other Paths: " + Arrays.toString(current.otherPaths());
|
||||
|
||||
logger.error(expected);
|
||||
logger.error(actual);
|
||||
}
|
||||
|
||||
private static List<PVCameraInfo> filterAllowedDevices(List<PVCameraInfo> allDevices) {
|
||||
Platform platform = Platform.getCurrentPlatform();
|
||||
ArrayList<PVCameraInfo> filteredDevices = new ArrayList<>();
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
@@ -273,4 +274,55 @@ public class VisionSourceManagerTest {
|
||||
assertEquals(2, vsm.getVsmState().disabledConfigs.size());
|
||||
assertEquals(1, vsm.vmm.getModules().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMismatch() throws InterruptedException {
|
||||
var vsm = new TestVsm();
|
||||
|
||||
// Create a saved camera configuration that expects a device at /dev/video0 with a name
|
||||
PVCameraInfo savedInfo =
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
new UsbCameraInfo(
|
||||
0, "/dev/video0", "CamA", new String[] {"/dev/v4l/by-path/1"}, 111, 222));
|
||||
CameraConfiguration savedConf = new CameraConfiguration(savedInfo);
|
||||
savedConf.deactivated = false;
|
||||
savedConf.nickname = "SavedCam";
|
||||
|
||||
// Register the saved config so VSM creates a VisionModule
|
||||
vsm.registerLoadedConfigs(List.of(savedConf));
|
||||
|
||||
// Now simulate a connected camera at same uniquePath but with a different name (mismatch)
|
||||
List<PVCameraInfo> currentInfo =
|
||||
List.of(
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
new UsbCameraInfo(
|
||||
0,
|
||||
"/dev/video0",
|
||||
"CamDifferent",
|
||||
new String[] {"/dev/v4l/by-path/1"},
|
||||
111,
|
||||
222)));
|
||||
|
||||
// Trigger state evaluation
|
||||
vsm.checkMismatches(currentInfo);
|
||||
|
||||
// The module should have detected a mismatch
|
||||
assertTrue(vsm.getVisionModules().stream().anyMatch(m -> m.mismatch));
|
||||
|
||||
// Now simulate the device being disconnected
|
||||
currentInfo = List.of();
|
||||
vsm.checkMismatches(currentInfo);
|
||||
|
||||
// Mismatch should be cleared when device is disconnected
|
||||
assertFalse(vsm.getVisionModules().stream().anyMatch(m -> m.mismatch));
|
||||
|
||||
// Test with a matching camera info
|
||||
currentInfo = List.of(savedInfo);
|
||||
vsm.checkMismatches(currentInfo);
|
||||
|
||||
// The mismatch should be cleared
|
||||
assertFalse(vsm.getVisionModules().stream().anyMatch(m -> m.mismatch));
|
||||
|
||||
vsm.teardown();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user