2025-01-01 03:04:20 -05:00
|
|
|
<script setup lang="ts">
|
|
|
|
|
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
2025-01-14 02:30:25 -05:00
|
|
|
import { computed, inject, ref } from "vue";
|
2025-01-01 03:04:20 -05:00
|
|
|
import { useStateStore } from "@/stores/StateStore";
|
|
|
|
|
import {
|
|
|
|
|
PlaceholderCameraSettings,
|
|
|
|
|
PVCameraInfo,
|
|
|
|
|
type PVCSICameraInfo,
|
|
|
|
|
type PVFileCameraInfo,
|
2025-01-07 08:45:39 -05:00
|
|
|
type PVUsbCameraInfo,
|
|
|
|
|
type UiCameraConfiguration
|
2025-01-01 03:04:20 -05:00
|
|
|
} from "@/types/SettingTypes";
|
|
|
|
|
import { getResolutionString } from "@/lib/PhotonUtils";
|
|
|
|
|
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
|
|
|
|
|
import axios from "axios";
|
2025-01-07 08:45:39 -05:00
|
|
|
import PvCameraMatchCard from "@/components/common/pv-camera-match-card.vue";
|
|
|
|
|
import type { WebsocketCameraSettingsUpdate } from "@/types/WebsocketDataTypes";
|
2025-01-01 03:04:20 -05:00
|
|
|
|
|
|
|
|
const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
|
|
|
|
|
const host = inject<string>("backendHost");
|
|
|
|
|
|
2025-01-07 08:45:39 -05:00
|
|
|
const activatingModule = ref(false);
|
2025-01-01 03:04:20 -05:00
|
|
|
const activateModule = (moduleUniqueName: string) => {
|
2025-01-07 08:45:39 -05:00
|
|
|
if (activatingModule.value) return;
|
|
|
|
|
activatingModule.value = true;
|
2025-01-01 03:04:20 -05:00
|
|
|
const url = new URL(`http://${host}/api/utils/activateMatchedCamera`);
|
2025-01-03 18:50:25 -05:00
|
|
|
url.searchParams.set("cameraUniqueName", moduleUniqueName);
|
2025-01-01 03:04:20 -05:00
|
|
|
|
|
|
|
|
fetch(url.toString(), {
|
|
|
|
|
method: "POST"
|
2025-01-14 02:30:25 -05:00
|
|
|
}).finally(() => (activatingModule.value = false));
|
2025-01-01 03:04:20 -05:00
|
|
|
};
|
2025-01-07 08:45:39 -05:00
|
|
|
|
|
|
|
|
const assigningCamera = ref(false);
|
|
|
|
|
const assignCamera = (cameraInfo: PVCameraInfo) => {
|
|
|
|
|
if (assigningCamera.value) return;
|
|
|
|
|
assigningCamera.value = true;
|
2025-01-01 03:04:20 -05:00
|
|
|
const url = new URL(`http://${host}/api/utils/assignUnmatchedCamera`);
|
|
|
|
|
url.searchParams.set("cameraInfo", JSON.stringify(cameraInfo));
|
|
|
|
|
|
|
|
|
|
fetch(url.toString(), {
|
|
|
|
|
method: "POST"
|
2025-01-14 02:30:25 -05:00
|
|
|
}).finally(() => (assigningCamera.value = false));
|
2025-01-01 03:04:20 -05:00
|
|
|
};
|
2025-01-07 08:45:39 -05:00
|
|
|
|
|
|
|
|
const deactivatingModule = ref(false);
|
|
|
|
|
const deactivateModule = (cameraUniqueName: string) => {
|
|
|
|
|
if (deactivatingModule.value) return;
|
|
|
|
|
deactivatingModule.value = true;
|
2025-01-01 03:04:20 -05:00
|
|
|
const url = new URL(`http://${host}/api/utils/unassignCamera`);
|
2025-01-03 18:50:25 -05:00
|
|
|
url.searchParams.set("cameraUniqueName", cameraUniqueName);
|
2025-01-01 03:04:20 -05:00
|
|
|
|
|
|
|
|
fetch(url.toString(), {
|
|
|
|
|
method: "POST"
|
2025-01-07 08:45:39 -05:00
|
|
|
}).finally(() => (deactivatingModule.value = false));
|
2025-01-01 03:04:20 -05:00
|
|
|
};
|
|
|
|
|
|
2025-01-07 08:45:39 -05:00
|
|
|
const deletingCamera = ref(false);
|
2025-01-01 03:04:20 -05:00
|
|
|
const deleteThisCamera = (cameraName: string) => {
|
2025-01-07 08:45:39 -05:00
|
|
|
if (deletingCamera.value) return;
|
|
|
|
|
deletingCamera.value = true;
|
2025-01-01 03:04:20 -05:00
|
|
|
const payload = {
|
|
|
|
|
cameraUniqueName: cameraName
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
axios
|
|
|
|
|
.post("/utils/nukeOneCamera", payload)
|
|
|
|
|
.then(() => {
|
|
|
|
|
useStateStore().showSnackbarMessage({
|
2025-01-07 08:45:39 -05:00
|
|
|
message: "Camera deleted successfully",
|
2025-01-01 03:04:20 -05:00
|
|
|
color: "success"
|
|
|
|
|
});
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
|
|
|
|
if (error.response) {
|
|
|
|
|
useStateStore().showSnackbarMessage({
|
|
|
|
|
message: "The backend is unable to fulfil the request to delete this camera.",
|
|
|
|
|
color: "error"
|
|
|
|
|
});
|
|
|
|
|
} else if (error.request) {
|
|
|
|
|
useStateStore().showSnackbarMessage({
|
|
|
|
|
message: "Error while trying to process the request! The backend didn't respond.",
|
|
|
|
|
color: "error"
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
useStateStore().showSnackbarMessage({
|
|
|
|
|
message: "An error occurred while trying to process the request.",
|
|
|
|
|
color: "error"
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-01-07 08:45:39 -05:00
|
|
|
})
|
|
|
|
|
.finally(() => {
|
|
|
|
|
setCameraDeleting(null);
|
|
|
|
|
deletingCamera.value = false;
|
2025-01-01 03:04:20 -05:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-07 08:45:39 -05:00
|
|
|
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;
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-14 02:30:25 -05:00
|
|
|
const cameraInfoFor = (camera: PVCameraInfo | null): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
|
|
|
|
|
if (!camera) return null;
|
2025-01-01 03:04:20 -05:00
|
|
|
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 {
|
2025-01-14 02:30:25 -05:00
|
|
|
PVFileCameraInfo: undefined,
|
2025-01-01 03:04:20 -05:00
|
|
|
PVCSICameraInfo: undefined,
|
|
|
|
|
PVUsbCameraInfo: undefined
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return (
|
|
|
|
|
useStateStore().vsmState.allConnectedCameras.find(
|
2025-01-14 02:30:25 -05:00
|
|
|
(it) => cameraInfoFor(it).uniquePath === cameraInfoFor(info).uniquePath
|
2025-01-01 03:04:20 -05:00
|
|
|
) || {
|
2025-01-14 02:30:25 -05:00
|
|
|
PVFileCameraInfo: undefined,
|
2025-01-01 03:04:20 -05:00
|
|
|
PVCSICameraInfo: undefined,
|
|
|
|
|
PVUsbCameraInfo: undefined
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-14 02:30:25 -05:00
|
|
|
const cameraCononected = (uniquePath: string): boolean => {
|
|
|
|
|
return (
|
|
|
|
|
useStateStore().vsmState.allConnectedCameras.find((it) => cameraInfoFor(it).uniquePath === uniquePath) !== undefined
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2025-01-01 03:04:20 -05:00
|
|
|
const unmatchedCameras = computed(() => {
|
2025-01-14 02:30:25 -05:00
|
|
|
const activeVmPaths = Object.values(useCameraSettingsStore().cameras).map(
|
|
|
|
|
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
|
2025-01-03 18:50:25 -05:00
|
|
|
);
|
2025-01-14 02:30:25 -05:00
|
|
|
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map(
|
|
|
|
|
(it) => cameraInfoFor(it.matchedCameraInfo).uniquePath
|
2025-01-01 03:04:20 -05:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return useStateStore().vsmState.allConnectedCameras.filter(
|
2025-01-14 02:30:25 -05:00
|
|
|
(it) =>
|
|
|
|
|
!activeVmPaths.includes(cameraInfoFor(it).uniquePath) && !disabledVmPaths.includes(cameraInfoFor(it).uniquePath)
|
2025-01-01 03:04:20 -05:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const activeVisionModules = computed(() =>
|
2025-01-14 02:30:25 -05:00
|
|
|
Object.values(useCameraSettingsStore().cameras)
|
|
|
|
|
// Ignore placeholder camera
|
|
|
|
|
.filter((camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings))
|
|
|
|
|
// Display connected cameras first
|
|
|
|
|
.sort(
|
|
|
|
|
(first, second) =>
|
|
|
|
|
(cameraCononected(cameraInfoFor(second.matchedCameraInfo).uniquePath) ? 1 : 0) -
|
|
|
|
|
(cameraCononected(cameraInfoFor(first.matchedCameraInfo).uniquePath) ? 1 : 0)
|
|
|
|
|
)
|
2025-01-01 03:04:20 -05:00
|
|
|
);
|
2025-01-14 02:30:25 -05:00
|
|
|
|
2025-01-01 03:04:20 -05:00
|
|
|
const disabledVisionModules = computed(() => useStateStore().vsmState.disabledConfigs);
|
|
|
|
|
|
|
|
|
|
const viewingDetails = ref(false);
|
2025-01-14 02:30:25 -05:00
|
|
|
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];
|
2025-01-01 03:04:20 -05:00
|
|
|
};
|
2025-01-07 08:45:39 -05:00
|
|
|
|
|
|
|
|
const viewingDeleteCamera = ref(false);
|
|
|
|
|
const cameraToDelete = ref<UiCameraConfiguration | WebsocketCameraSettingsUpdate | null>(null);
|
|
|
|
|
const setCameraDeleting = (camera: UiCameraConfiguration | WebsocketCameraSettingsUpdate | null) => {
|
|
|
|
|
yesDeleteMySettingsText.value = "";
|
|
|
|
|
viewingDeleteCamera.value = camera !== null;
|
|
|
|
|
cameraToDelete.value = camera;
|
|
|
|
|
};
|
|
|
|
|
const yesDeleteMySettingsText = ref("");
|
|
|
|
|
const exportSettings = ref();
|
|
|
|
|
const openExportSettingsPrompt = () => {
|
|
|
|
|
exportSettings.value.click();
|
|
|
|
|
};
|
2025-01-01 03:04:20 -05:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="pa-5">
|
|
|
|
|
<v-row>
|
|
|
|
|
<!-- Active modules -->
|
2025-01-07 08:45:39 -05:00
|
|
|
<v-col
|
|
|
|
|
v-for="(module, index) in activeVisionModules"
|
|
|
|
|
:key="`enabled-${module.uniqueName}`"
|
|
|
|
|
cols="12"
|
|
|
|
|
sm="6"
|
|
|
|
|
lg="4"
|
|
|
|
|
>
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-card dark color="primary">
|
2025-01-07 08:45:39 -05:00
|
|
|
<v-card-title>{{ cameraInfoFor(module.matchedCameraInfo).name }}</v-card-title>
|
2025-01-14 02:30:25 -05:00
|
|
|
<v-card-subtitle v-if="!cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)" class="pb-2"
|
|
|
|
|
>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)
|
|
|
|
|
"
|
|
|
|
|
class="pb-2"
|
2025-01-01 03:04:20 -05:00
|
|
|
>Status: <span class="active-status">Active</span></v-card-subtitle
|
|
|
|
|
>
|
2025-01-14 02:30:25 -05:00
|
|
|
<v-card-subtitle v-else class="pb-2">Status: <span class="mismatch-status">Mismatch</span></v-card-subtitle>
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-card-text>
|
2025-01-14 02:30:25 -05:00
|
|
|
<v-simple-table dark dense>
|
2025-01-01 03:04:20 -05:00
|
|
|
<tbody>
|
|
|
|
|
<tr>
|
|
|
|
|
<td>Streams:</td>
|
|
|
|
|
<td>
|
2025-01-14 02:30:25 -05:00
|
|
|
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="stream-link"> Input </a>
|
2025-01-01 03:04:20 -05:00
|
|
|
/
|
2025-01-14 02:30:25 -05:00
|
|
|
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="stream-link"> Output </a>
|
2025-01-01 03:04:20 -05:00
|
|
|
</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>
|
2025-01-14 02:30:25 -05:00
|
|
|
<tr
|
|
|
|
|
v-if="
|
|
|
|
|
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) &&
|
|
|
|
|
useStateStore().backendResults[module.uniqueName]
|
|
|
|
|
"
|
|
|
|
|
>
|
|
|
|
|
<td style="width: 50%">Frames Processed</td>
|
2025-01-01 03:04:20 -05:00
|
|
|
<td>
|
2025-01-03 18:50:25 -05:00
|
|
|
{{ useStateStore().backendResults[module.uniqueName].sequenceID }} ({{
|
|
|
|
|
useStateStore().backendResults[module.uniqueName].fps
|
2025-01-01 03:04:20 -05:00
|
|
|
}}
|
|
|
|
|
FPS)
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</v-simple-table>
|
2025-01-07 08:45:39 -05:00
|
|
|
<div
|
2025-01-14 02:30:25 -05:00
|
|
|
v-if="cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)"
|
2025-01-07 08:45:39 -05:00
|
|
|
:id="`stream-container-${index}`"
|
2025-01-14 02:30:25 -05:00
|
|
|
class="d-flex flex-column justify-center align-center mt-3"
|
2025-01-07 08:45:39 -05:00
|
|
|
style="height: 250px"
|
|
|
|
|
>
|
|
|
|
|
<photon-camera-stream
|
|
|
|
|
:id="`output-camera-stream-${index}`"
|
|
|
|
|
:camera-settings="module"
|
|
|
|
|
stream-type="Processed"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-01-01 03:04:20 -05:00
|
|
|
</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">
|
2025-01-14 02:30:25 -05:00
|
|
|
<v-btn
|
|
|
|
|
color="secondary"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
@click="
|
|
|
|
|
setCameraView(
|
|
|
|
|
module.matchedCameraInfo,
|
|
|
|
|
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
|
|
|
|
|
)
|
|
|
|
|
"
|
|
|
|
|
>
|
2025-01-01 03:04:20 -05:00
|
|
|
<span>Details</span>
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-col>
|
|
|
|
|
<v-col cols="6" md="5" class="pr-0">
|
|
|
|
|
<v-btn
|
|
|
|
|
class="black--text"
|
|
|
|
|
color="accent"
|
|
|
|
|
style="width: 100%"
|
2025-01-07 08:45:39 -05:00
|
|
|
:loading="deactivatingModule"
|
|
|
|
|
@click="deactivateModule(module.uniqueName)"
|
2025-01-01 03:04:20 -05:00
|
|
|
>
|
|
|
|
|
Deactivate
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-col>
|
|
|
|
|
<v-col cols="6" md="3">
|
2025-01-07 08:45:39 -05:00
|
|
|
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-icon>mdi-trash-can-outline</v-icon>
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
</v-card-text>
|
|
|
|
|
</v-card>
|
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
|
|
<!-- Disabled modules -->
|
2025-01-03 08:29:18 -06:00
|
|
|
<v-col v-for="module in disabledVisionModules" :key="`disabled-${module.uniqueName}`" cols="12" sm="6" lg="4">
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-card dark color="primary">
|
|
|
|
|
<v-card-title>{{ module.nickname }}</v-card-title>
|
2025-01-14 02:30:25 -05:00
|
|
|
<v-card-subtitle class="pb-2">Status: <span class="inactive-status">Deactivated</span></v-card-subtitle>
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-card-text>
|
|
|
|
|
<v-simple-table dense>
|
|
|
|
|
<tbody>
|
|
|
|
|
<tr>
|
|
|
|
|
<td>Name</td>
|
|
|
|
|
<td>
|
|
|
|
|
{{ module.cameraQuirks.baseName }}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td>Pipelines</td>
|
|
|
|
|
<td>{{ module.pipelineNicknames.join(", ") }}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td>Connected</td>
|
2025-01-14 02:30:25 -05:00
|
|
|
<td>{{ cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath) }}</td>
|
2025-01-01 03:04:20 -05:00
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td>Calibrations</td>
|
|
|
|
|
<td>
|
|
|
|
|
{{
|
|
|
|
|
module.calibrations.map((it2) => getResolutionString(it2.resolution)).join(", ") ||
|
|
|
|
|
"Not calibrated"
|
|
|
|
|
}}
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</v-simple-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">
|
2025-01-14 02:30:25 -05:00
|
|
|
<v-btn
|
|
|
|
|
color="secondary"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
@click="
|
|
|
|
|
setCameraView(
|
|
|
|
|
module.matchedCameraInfo,
|
|
|
|
|
cameraCononected(cameraInfoFor(module.matchedCameraInfo).uniquePath)
|
|
|
|
|
)
|
|
|
|
|
"
|
|
|
|
|
>
|
2025-01-01 03:04:20 -05:00
|
|
|
<span>Details</span>
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-col>
|
|
|
|
|
<v-col cols="6" md="5" class="pr-0">
|
|
|
|
|
<v-btn
|
|
|
|
|
class="black--text"
|
|
|
|
|
color="accent"
|
|
|
|
|
style="width: 100%"
|
2025-01-07 08:45:39 -05:00
|
|
|
:loading="activatingModule"
|
2025-01-03 08:29:18 -06:00
|
|
|
@click="activateModule(module.uniqueName)"
|
2025-01-01 03:04:20 -05:00
|
|
|
>
|
|
|
|
|
Activate
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-col>
|
|
|
|
|
<v-col cols="6" md="3">
|
2025-01-07 08:45:39 -05:00
|
|
|
<v-btn class="pa-0" color="error" style="width: 100%" @click="setCameraDeleting(module)">
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-icon>mdi-trash-can-outline</v-icon>
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
</v-card-text>
|
|
|
|
|
</v-card>
|
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
|
|
<!-- Unassigned cameras -->
|
2025-01-03 08:29:18 -06:00
|
|
|
<v-col v-for="(camera, index) in unmatchedCameras" :key="index" cols="12" sm="6" lg="4">
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-card dark color="primary">
|
2025-01-14 02:30:25 -05:00
|
|
|
<v-card-title class="pb-2">
|
2025-01-01 03:04:20 -05:00
|
|
|
<span v-if="camera.PVUsbCameraInfo">USB Camera:</span>
|
|
|
|
|
<span v-else-if="camera.PVCSICameraInfo">CSI Camera:</span>
|
|
|
|
|
<span v-else-if="camera.PVFileCameraInfo">File Camera:</span>
|
|
|
|
|
<span v-else>Unknown Camera:</span>
|
|
|
|
|
<span>{{ cameraInfoFor(camera)?.name ?? cameraInfoFor(camera)?.baseName }}</span>
|
|
|
|
|
</v-card-title>
|
|
|
|
|
<v-card-subtitle>Status: Unassigned</v-card-subtitle>
|
|
|
|
|
<v-card-text>
|
|
|
|
|
<span style="word-break: break-all">{{ cameraInfoFor(camera)?.path }}</span>
|
|
|
|
|
</v-card-text>
|
|
|
|
|
<v-card-text class="pt-0">
|
|
|
|
|
<v-row>
|
|
|
|
|
<v-col cols="6" class="pr-0">
|
2025-01-14 02:30:25 -05:00
|
|
|
<v-btn color="secondary" style="width: 100%" @click="setCameraView(camera, false)">
|
2025-01-01 03:04:20 -05:00
|
|
|
<span>Details</span>
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-col>
|
|
|
|
|
<v-col cols="6">
|
2025-01-07 08:45:39 -05:00
|
|
|
<v-btn
|
|
|
|
|
class="black--text"
|
|
|
|
|
color="accent"
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
:loading="assigningCamera"
|
|
|
|
|
@click="assignCamera(camera)"
|
|
|
|
|
>
|
2025-01-01 03:04:20 -05:00
|
|
|
Activate
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
</v-card-text>
|
|
|
|
|
</v-card>
|
|
|
|
|
</v-col>
|
|
|
|
|
|
|
|
|
|
<!-- Info card -->
|
|
|
|
|
<v-col cols="12" sm="6" lg="4">
|
|
|
|
|
<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">
|
|
|
|
|
<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 -->
|
2025-01-07 08:45:39 -05:00
|
|
|
<v-dialog v-model="viewingDetails" max-width="800">
|
2025-01-14 02:30:25 -05:00
|
|
|
<v-card v-if="viewingCamera[0] !== null" dark flat color="primary">
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-card-title class="d-flex justify-space-between">
|
2025-01-14 02:30:25 -05:00
|
|
|
<span>{{ cameraInfoFor(viewingCamera[0])?.name ?? cameraInfoFor(viewingCamera[0])?.baseName }}</span>
|
|
|
|
|
<v-btn text @click="setCameraView(null, null)">
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-icon>mdi-close-thick</v-icon>
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-card-title>
|
2025-01-14 02:30:25 -05:00
|
|
|
<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])">
|
2025-01-07 08:45:39 -05:00
|
|
|
<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>
|
2025-01-14 02:30:25 -05:00
|
|
|
<PvCameraMatchCard :saved="viewingCamera[0]" :current="getMatchedDevice(viewingCamera[0])" />
|
2025-01-07 08:45:39 -05:00
|
|
|
</v-card-text>
|
|
|
|
|
<v-card-text v-else>
|
2025-01-14 02:30:25 -05:00
|
|
|
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera[0])" />
|
2025-01-07 08:45:39 -05:00
|
|
|
</v-card-text>
|
|
|
|
|
</v-card>
|
|
|
|
|
</v-dialog>
|
|
|
|
|
|
|
|
|
|
<!-- Camera delete modal -->
|
|
|
|
|
<v-dialog v-model="viewingDeleteCamera" dark width="800">
|
|
|
|
|
<v-card v-if="cameraToDelete !== null" dark class="dialog-container pa-3 pb-2" color="primary" flat>
|
|
|
|
|
<v-card-title> Delete {{ cameraToDelete.nickname }}? </v-card-title>
|
|
|
|
|
<v-card-text>
|
|
|
|
|
<v-row class="align-center pt-6">
|
|
|
|
|
<v-col cols="12" md="6">
|
|
|
|
|
<span class="white--text"> This will delete ALL OF YOUR SETTINGS and restart PhotonVision. </span>
|
|
|
|
|
</v-col>
|
|
|
|
|
<v-col cols="12" md="6">
|
|
|
|
|
<v-btn color="secondary" block @click="openExportSettingsPrompt">
|
|
|
|
|
<v-icon left class="open-icon"> mdi-export </v-icon>
|
|
|
|
|
<span class="open-label">Backup Settings</span>
|
|
|
|
|
<a
|
|
|
|
|
ref="exportSettings"
|
|
|
|
|
style="color: black; text-decoration: none; display: none"
|
|
|
|
|
:href="`http://${host}/api/settings/photonvision_config.zip`"
|
|
|
|
|
download="photonvision-settings.zip"
|
|
|
|
|
target="_blank"
|
|
|
|
|
/>
|
|
|
|
|
</v-btn>
|
|
|
|
|
</v-col>
|
|
|
|
|
</v-row>
|
|
|
|
|
</v-card-text>
|
2025-01-01 03:04:20 -05:00
|
|
|
<v-card-text>
|
2025-01-07 08:45:39 -05:00
|
|
|
<pv-input
|
|
|
|
|
v-model="yesDeleteMySettingsText"
|
|
|
|
|
:label="'Type "' + cameraToDelete.nickname + '":'"
|
|
|
|
|
:label-cols="6"
|
|
|
|
|
:input-cols="6"
|
|
|
|
|
/>
|
|
|
|
|
</v-card-text>
|
|
|
|
|
<v-card-text>
|
|
|
|
|
<v-btn
|
|
|
|
|
block
|
|
|
|
|
color="error"
|
|
|
|
|
:disabled="yesDeleteMySettingsText.toLowerCase() !== cameraToDelete.nickname.toLowerCase()"
|
|
|
|
|
:loading="deletingCamera"
|
|
|
|
|
@click="deleteThisCamera(cameraToDelete.uniqueName)"
|
2025-01-01 03:04:20 -05:00
|
|
|
>
|
2025-01-07 08:45:39 -05:00
|
|
|
<v-icon left class="open-icon"> mdi-trash-can-outline </v-icon>
|
|
|
|
|
<span class="open-label">DELETE (UNRECOVERABLE)</span>
|
|
|
|
|
</v-btn>
|
2025-01-01 03:04:20 -05:00
|
|
|
</v-card-text>
|
|
|
|
|
</v-card>
|
|
|
|
|
</v-dialog>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
.v-data-table {
|
|
|
|
|
background-color: #006492 !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 {
|
|
|
|
|
color: pink;
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
a:active,
|
2025-01-07 08:45:39 -05:00
|
|
|
.stream-link,
|
2025-01-01 03:04:20 -05:00
|
|
|
.mismatch-status {
|
|
|
|
|
color: yellow;
|
|
|
|
|
background-color: transparent;
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
}
|
|
|
|
|
</style>
|