Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion photon-client/src/stores/settings/CameraSettingsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}, {});
Expand Down
4 changes: 3 additions & 1 deletion photon-client/src/types/SettingTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export interface UiCameraConfiguration {
matchedCameraInfo: PVCameraInfo;
isConnected: boolean;
hasConnected: boolean;
mismatch: boolean;
}

export interface CameraSettingsChangeRequest {
Expand Down Expand Up @@ -388,7 +389,8 @@ export const PlaceholderCameraSettings: UiCameraConfiguration = {
PVUsbCameraInfo: undefined
},
isConnected: true,
hasConnected: true
hasConnected: true,
mismatch: false
};

export enum CalibrationBoardTypes {
Expand Down
1 change: 1 addition & 0 deletions photon-client/src/types/WebsocketDataTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export interface WebsocketCameraSettingsUpdate {
matchedCameraInfo: PVCameraInfo;
isConnected: boolean;
hasConnected: boolean;
mismatch: boolean;
}
export interface WebsocketNTUpdate {
connected: boolean;
Expand Down
127 changes: 56 additions & 71 deletions photon-client/src/views/CameraMatchingView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
Expand All @@ -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)
)
);

Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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]
"
>
Expand Down Expand Up @@ -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"
Expand All @@ -370,7 +349,7 @@ const yesDeleteMySettingsText = ref("");
@click="
setCameraView(
module.matchedCameraInfo,
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
>
Expand Down Expand Up @@ -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>
Expand All @@ -456,7 +435,7 @@ const yesDeleteMySettingsText = ref("");
@click="
setCameraView(
module.matchedCameraInfo,
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
cameraConnected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
)
"
>
Expand Down Expand Up @@ -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"
Expand Down
27 changes: 27 additions & 0 deletions photon-client/src/views/DashboardView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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[] => {
Expand Down Expand Up @@ -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;
});
Expand Down Expand Up @@ -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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -95,6 +97,7 @@ private 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
Expand All @@ -115,6 +118,14 @@ public static NetworkTablesManager getInstance() {
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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)
Expand Down Expand Up @@ -70,8 +71,15 @@ default String humanReadableName() {

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")
Expand Down Expand Up @@ -125,7 +133,17 @@ 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 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
Expand Down Expand Up @@ -191,7 +209,14 @@ 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 false;

return baseName.equals(info.baseName) && path.equals(info.path);
}

@Override
public int hashCode() {
return Objects.hash(baseName, path);
}

@Override
Expand Down Expand Up @@ -248,7 +273,14 @@ 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 false;

return name.equals(info.name) && path.equals(info.path);
}

@Override
public int hashCode() {
return Objects.hash(name, path);
}

@Override
Expand Down
Loading
Loading