mirror of
https://github.com/PhotonVision/photonvision
synced 2026-06-19 00:41:41 +00:00
## Description WPILib switched from FasterXML Jackson to Avaje Jsonb for speed reasons in https://github.com/wpilibsuite/allwpilib/pull/8721. This does the same for PhotonVision. Some temporary Jackson adapters are present to allow compatibility with alpha-4 ahead of updating Photon's WPILib version. A few old backwards compatibility migrations were also dropped if they were difficult to port to Avaje Jsonb or otherwise complicated the code. ## 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_, including events that led to this PR - [ ] 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 - [ ] If this PR touches configuration, this is backwards compatible with all settings going back to the previous seasons's last release (seasons end after champs ends) - [ ] 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 - [ ] If this PR adds a dependency, the license has been checked for compatibility and steps taken to follow it --------- Co-authored-by: samfreund <samf.236@proton.me> Co-authored-by: Matt Morley <matthew.morley.ca@gmail.com>
477 lines
17 KiB
Vue
477 lines
17 KiB
Vue
<script setup lang="ts">
|
|
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
|
import { computed, inject, ref } from "vue";
|
|
import { useStateStore } from "@/stores/StateStore";
|
|
import { PlaceholderCameraSettings, type PVCameraInfo } from "@/types/SettingTypes";
|
|
import { axiosPost, getResolutionString } from "@/lib/PhotonUtils";
|
|
import PhotonCameraStream from "@/components/app/photon-camera-stream.vue";
|
|
import PvDeleteModal from "@/components/common/pv-delete-modal.vue";
|
|
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
|
|
import PvCameraMatchCard from "@/components/common/pv-camera-match-card.vue";
|
|
import { useTheme } from "vuetify";
|
|
|
|
const theme = useTheme();
|
|
|
|
const backendHostname = inject<string>("backendHostname");
|
|
const formatUrl = (port: number) => `http://${backendHostname}:${port}/stream.mjpg`;
|
|
|
|
const activatingModule = ref(false);
|
|
const activateModule = async (moduleUniqueName: string) => {
|
|
if (activatingModule.value) return;
|
|
activatingModule.value = true;
|
|
|
|
await axiosPost("/utils/activateMatchedCamera", "activate a matched camera", {
|
|
cameraUniqueName: moduleUniqueName
|
|
});
|
|
activatingModule.value = false;
|
|
};
|
|
|
|
const assigningCamera = ref(false);
|
|
const assignCamera = async (cameraInfo: PVCameraInfo) => {
|
|
if (assigningCamera.value) return;
|
|
assigningCamera.value = true;
|
|
|
|
const payload = {
|
|
cameraInfo: cameraInfo
|
|
};
|
|
|
|
await axiosPost("/utils/assignUnmatchedCamera", "assign an unmatched camera", payload);
|
|
assigningCamera.value = false;
|
|
};
|
|
|
|
const deactivatingModule = ref(false);
|
|
const deactivateModule = async (cameraUniqueName: string) => {
|
|
if (deactivatingModule.value) return;
|
|
deactivatingModule.value = true;
|
|
await axiosPost("/utils/unassignCamera", "unassign a camera", { cameraUniqueName: cameraUniqueName });
|
|
deactivatingModule.value = false;
|
|
};
|
|
|
|
const confirmDeleteDialog = ref({ show: false, nickname: "", cameraUniqueName: "" });
|
|
const deletingCamera = ref<string | null>(null);
|
|
|
|
const deleteThisCamera = async (cameraUniqueName: string) => {
|
|
if (deletingCamera.value) return;
|
|
deletingCamera.value = cameraUniqueName;
|
|
await axiosPost("/utils/nukeOneCamera", "delete a camera", { cameraUniqueName: cameraUniqueName });
|
|
deletingCamera.value = null;
|
|
};
|
|
|
|
const cameraConnected = (uniquePath: string | undefined): boolean => {
|
|
if (!uniquePath) return false;
|
|
return useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === uniquePath) !== undefined;
|
|
};
|
|
|
|
const unmatchedCameras = computed(() => {
|
|
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map((it) => it.matchedCameraInfo.uniquePath);
|
|
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map((it) => it.matchedCameraInfo.uniquePath);
|
|
|
|
return useStateStore().vsmState.allConnectedCameras.filter(
|
|
(it) => !activeVmPaths.includes(it.uniquePath) && !disabledVmPaths.includes(it.uniquePath)
|
|
);
|
|
});
|
|
|
|
const activeVisionModules = computed(() =>
|
|
Object.values(useCameraSettingsStore().cameras)
|
|
// Ignore placeholder camera
|
|
.filter((camera) => camera !== PlaceholderCameraSettings)
|
|
// Display connected cameras first
|
|
.sort(
|
|
(first, second) =>
|
|
(cameraConnected(second.matchedCameraInfo.uniquePath) ? 1 : 0) -
|
|
(cameraConnected(first.matchedCameraInfo.uniquePath) ? 1 : 0)
|
|
)
|
|
);
|
|
|
|
const disabledVisionModules = computed(() => useStateStore().vsmState.disabledConfigs);
|
|
|
|
const viewingDetails = ref(false);
|
|
const viewingCamera = ref<[PVCameraInfo | null, boolean | null]>([null, null]);
|
|
const setCameraView = (camera: PVCameraInfo | null, isConnected: boolean | null) => {
|
|
viewingDetails.value = camera !== null && isConnected !== null;
|
|
viewingCamera.value = [camera, isConnected];
|
|
};
|
|
|
|
/**
|
|
* Find the PVCameraInfo currently occupying the same uniquePath as the the given module
|
|
*/
|
|
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
|
if (!info) {
|
|
return {
|
|
type: "PVFileCameraInfo",
|
|
path: "",
|
|
name: "",
|
|
uniquePath: ""
|
|
};
|
|
}
|
|
return (
|
|
useStateStore().vsmState.allConnectedCameras.find((it) => it.uniquePath === info.uniquePath) || {
|
|
type: "PVFileCameraInfo",
|
|
path: "",
|
|
name: "",
|
|
uniquePath: ""
|
|
}
|
|
);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="pa-3">
|
|
<v-row>
|
|
<!-- Active modules -->
|
|
<v-col
|
|
v-for="(module, index) in activeVisionModules"
|
|
:key="`enabled-${module.uniqueName}`"
|
|
cols="12"
|
|
sm="6"
|
|
lg="4"
|
|
class="pr-0"
|
|
>
|
|
<v-card color="surface" class="rounded-12">
|
|
<v-card-title>{{ module.matchedCameraInfo.name }}</v-card-title>
|
|
<v-card-subtitle v-if="!cameraConnected(module.matchedCameraInfo.uniquePath)"
|
|
>Status: <span class="inactive-status">Disconnected</span></v-card-subtitle
|
|
>
|
|
<v-card-subtitle v-else-if="cameraConnected(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>
|
|
<v-card-text class="pt-3">
|
|
<v-table density="compact">
|
|
<tbody>
|
|
<tr
|
|
v-if="
|
|
cameraConnected(module.matchedCameraInfo.uniquePath) &&
|
|
useStateStore().backendResults[module.uniqueName]
|
|
"
|
|
>
|
|
<td style="width: 50%">Frames Processed</td>
|
|
<td>
|
|
{{ useStateStore().backendResults[module.uniqueName].sequenceID }} ({{
|
|
useStateStore().backendResults[module.uniqueName].fps
|
|
}}
|
|
FPS)
|
|
</td>
|
|
</tr>
|
|
<tr v-else>
|
|
<td>Name</td>
|
|
<td>
|
|
{{ module.nickname }}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Pipelines</td>
|
|
<td>{{ module.pipelineNicknames.join(", ") }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Calibrations</td>
|
|
<td>
|
|
{{
|
|
module.completeCalibrations.map((it) => getResolutionString(it.resolution)).join(", ") ||
|
|
"Not calibrated"
|
|
}}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Streams:</td>
|
|
<td>
|
|
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="stream-link"> Input </a>
|
|
/
|
|
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="stream-link"> Output </a>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
<div
|
|
v-if="cameraConnected(module.matchedCameraInfo.uniquePath)"
|
|
:id="`stream-container-${index}`"
|
|
class="d-flex flex-column justify-center align-center mt-3"
|
|
style="height: 250px"
|
|
>
|
|
<photon-camera-stream
|
|
:id="`output-camera-stream-${index}`"
|
|
:camera-settings="module"
|
|
stream-type="Processed"
|
|
/>
|
|
</div>
|
|
</v-card-text>
|
|
<v-card-text class="pt-0">
|
|
<v-row>
|
|
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
|
|
<v-btn
|
|
color="buttonPassive"
|
|
style="width: 100%"
|
|
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
|
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
|
|
>
|
|
<span>Details</span>
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col cols="6" md="5" class="pr-0">
|
|
<v-btn
|
|
class="text-black"
|
|
color="buttonActive"
|
|
style="width: 100%"
|
|
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
|
:loading="deactivatingModule"
|
|
@click="deactivateModule(module.uniqueName)"
|
|
>
|
|
Deactivate
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col cols="6" md="3">
|
|
<v-btn
|
|
class="pa-0"
|
|
color="error"
|
|
style="width: 100%"
|
|
:loading="module.uniqueName === deletingCamera"
|
|
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
|
@click="
|
|
() =>
|
|
(confirmDeleteDialog = {
|
|
show: true,
|
|
nickname: module.nickname,
|
|
cameraUniqueName: module.uniqueName
|
|
})
|
|
"
|
|
>
|
|
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Deactivated modules -->
|
|
<v-col
|
|
v-for="module in disabledVisionModules"
|
|
:key="`disabled-${module.uniqueName}`"
|
|
cols="12"
|
|
sm="6"
|
|
lg="4"
|
|
class="pr-0"
|
|
>
|
|
<v-card class="pr-0 rounded-12" color="surface">
|
|
<v-card-title>{{ module.cameraQuirks.baseName }}</v-card-title>
|
|
<v-card-subtitle>Status: <span class="inactive-status">Deactivated</span></v-card-subtitle>
|
|
<v-card-text class="pt-3">
|
|
<v-table density="compact">
|
|
<tbody>
|
|
<tr>
|
|
<td>Name</td>
|
|
<td>
|
|
{{ module.nickname }}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Pipelines</td>
|
|
<td>{{ module.pipelineNicknames.join(", ") }}</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Calibrations</td>
|
|
<td>
|
|
{{
|
|
module.calibrations.map((it2) => getResolutionString(it2.resolution)).join(", ") ||
|
|
"Not calibrated"
|
|
}}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td>Connected</td>
|
|
<td>{{ cameraConnected(module.matchedCameraInfo.uniquePath) }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</v-table>
|
|
</v-card-text>
|
|
<v-card-text class="pt-0">
|
|
<v-row>
|
|
<v-col cols="12" md="4" class="pr-md-0 pb-0 pb-md-3">
|
|
<v-btn
|
|
color="buttonPassive"
|
|
style="width: 100%"
|
|
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
|
@click="setCameraView(module.matchedCameraInfo, cameraConnected(module.matchedCameraInfo.uniquePath))"
|
|
>
|
|
<span>Details</span>
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col cols="6" md="5" class="pr-0">
|
|
<v-btn
|
|
class="text-black"
|
|
color="buttonActive"
|
|
style="width: 100%"
|
|
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
|
:loading="activatingModule"
|
|
@click="activateModule(module.uniqueName)"
|
|
>
|
|
Activate
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col cols="6" md="3">
|
|
<v-btn
|
|
class="pa-0"
|
|
color="error"
|
|
style="width: 100%"
|
|
:loading="module.uniqueName === deletingCamera"
|
|
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
|
@click="
|
|
() =>
|
|
(confirmDeleteDialog = {
|
|
show: true,
|
|
nickname: module.nickname,
|
|
cameraUniqueName: module.uniqueName
|
|
})
|
|
"
|
|
>
|
|
<v-icon size="x-large">mdi-trash-can-outline</v-icon>
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Unassigned cameras -->
|
|
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4" class="pr-0">
|
|
<v-card class="pr-0 rounded-12" color="surface">
|
|
<v-card-title>
|
|
<span v-if="camera.type === 'PVUsbCameraInfo'">USB Camera:</span>
|
|
<span v-else-if="camera.type === 'PVCSICameraInfo'">CSI Camera:</span>
|
|
<span v-else-if="camera.type === 'PVFileCameraInfo'">File Camera:</span>
|
|
<span v-else>Unknown Camera:</span>
|
|
<span>{{ camera.name }}</span>
|
|
</v-card-title>
|
|
<v-card-subtitle>Status: Unassigned</v-card-subtitle>
|
|
<v-card-text class="pt-3">
|
|
<span style="word-break: break-all">{{ camera?.path }}</span>
|
|
</v-card-text>
|
|
<v-card-text class="pt-0">
|
|
<v-row>
|
|
<v-col cols="6" class="pr-0">
|
|
<v-btn
|
|
color="buttonPassive"
|
|
style="width: 100%"
|
|
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
|
@click="setCameraView(camera, false)"
|
|
>
|
|
<span>Details</span>
|
|
</v-btn>
|
|
</v-col>
|
|
<v-col cols="6">
|
|
<v-btn
|
|
class="text-black"
|
|
color="buttonActive"
|
|
style="width: 100%"
|
|
:loading="assigningCamera"
|
|
:variant="theme.global.current.value.dark ? 'outlined' : 'elevated'"
|
|
@click="assignCamera(camera)"
|
|
>
|
|
Activate
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-col>
|
|
|
|
<!-- Info card -->
|
|
<v-col cols="12" sm="6" lg="4" class="pr-0">
|
|
<v-card
|
|
dark
|
|
flat
|
|
class="pl-6 pr-6 d-flex flex-column justify-center"
|
|
style="background-color: transparent; height: 100%"
|
|
>
|
|
<v-card-text class="d-flex flex-column align-center justify-center" style="flex-grow: 0">
|
|
<v-icon size="64" color="primary">mdi-plus</v-icon>
|
|
</v-card-text>
|
|
<v-card-title>Additional plugged in cameras will display here!</v-card-title>
|
|
</v-card>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Camera details modal -->
|
|
<v-dialog v-model="viewingDetails" max-width="800">
|
|
<v-card v-if="viewingCamera[0] !== null" flat color="surface">
|
|
<v-card-title class="d-flex justify-space-between">
|
|
<span>{{ viewingCamera[0].name }}</span>
|
|
<v-btn variant="text" @click="setCameraView(null, null)">
|
|
<v-icon size="x-large">mdi-close</v-icon>
|
|
</v-btn>
|
|
</v-card-title>
|
|
<v-card-text v-if="!viewingCamera[1]">
|
|
<PvCameraInfoCard :camera="viewingCamera[0]" />
|
|
</v-card-text>
|
|
<v-card-text
|
|
v-else-if="
|
|
activeVisionModules.find((it) => it.matchedCameraInfo.uniquePath === viewingCamera[0]?.uniquePath)?.mismatch
|
|
"
|
|
>
|
|
<v-alert
|
|
class="mb-3"
|
|
color="buttonActive"
|
|
density="compact"
|
|
text="A different camera may have been connected to this device! Compare the following information carefully."
|
|
icon="mdi-information-outline"
|
|
:variant="theme.global.current.value.dark ? 'tonal' : 'elevated'"
|
|
/>
|
|
<PvCameraMatchCard :saved="viewingCamera[0]" :current="getMatchedDevice(viewingCamera[0])" />
|
|
</v-card-text>
|
|
<v-card-text v-else>
|
|
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera[0])" />
|
|
</v-card-text>
|
|
</v-card>
|
|
</v-dialog>
|
|
|
|
<pv-delete-modal
|
|
v-model="confirmDeleteDialog.show"
|
|
title="Delete Camera"
|
|
:description="`Are you sure you want to delete the camera '${useCameraSettingsStore().currentCameraSettings.nickname}'? This action cannot be undone.`"
|
|
:expected-confirmation-text="confirmDeleteDialog.nickname"
|
|
:on-confirm="() => deleteThisCamera(confirmDeleteDialog.cameraUniqueName)"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
td {
|
|
padding: 0 !important;
|
|
}
|
|
|
|
.v-card-subtitle {
|
|
padding-top: 0px !important;
|
|
padding-bottom: 8px !important;
|
|
}
|
|
|
|
.v-card-title {
|
|
padding-bottom: 0 !important;
|
|
text-wrap-mode: wrap !important;
|
|
}
|
|
|
|
.active-status {
|
|
color: rgb(14, 240, 14);
|
|
background-color: transparent;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.inactive-status {
|
|
color: red;
|
|
background-color: transparent;
|
|
text-decoration: none;
|
|
}
|
|
|
|
a:hover {
|
|
background-color: transparent;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
a:active,
|
|
.stream-link,
|
|
.mismatch-status {
|
|
color: yellow;
|
|
background-color: transparent;
|
|
text-decoration: none;
|
|
}
|
|
</style>
|