mirror of
https://github.com/PhotonVision/photonvision
synced 2026-07-04 03:11:40 +00:00
Convert to user selected camera matching (#1556)
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
# Camera Matching
|
||||
|
||||
Diagrams generated by the [PlantUML UML editor](https://www.plantuml.com/plantuml/). Copy the image URLs below and decode in the editor to make changes.
|
||||
|
||||
## Initial Setup
|
||||
|
||||
When PhotonVision first starts, settings are loaded from disk and [VisionSources](https://javadocs.photonvision.org/org/photonvision/vision/processes/VisionSource.html) are created for every serialized & active [Camera Configuration](https://javadocs.photonvision.org/org/photonvision/common/configuration/CameraConfiguration.html)
|
||||
|
||||

|
||||
|
||||
## UI Workflow
|
||||
|
||||
A [background thread](https://javadocs.photonvision.org/org/photonvision/common/util/TimedTaskManager.html) will periodically query CSCore and Libcamera for what cameras we currently see connected. This list is provided to the web UI for display.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
This UI allows users to "Activate" a camera that's never been seen before, or activate a CameraConfiguration we've seen before but was disabled. Allowing camera configurations to be saved but not loaded by default lets us support temporarily disabling/unplugging a camera without flooding log files.
|
||||
|
||||
Since our backend logic intentionally does not protect users from plugging camera B into the port that camera A was active on, the UI shall show a warning but vision processing will (attempt to) continue like normal.
|
||||
|
||||
### Activate New Camera
|
||||
|
||||
When a new camera (ie, one we can't match by-path to a deserialized CameraConfiguration) is activated, we'll create a spin up a new Vision Module for it
|
||||
|
||||

|
||||
|
||||
### Deactivate Camera
|
||||
|
||||
Deactivating a camera will release the native resources it owns, and return the CameraConfiguration to the pool of currently disabled cameras we can re-enable later.
|
||||
|
||||

|
||||
|
||||
### Reactivate a CameraConfig
|
||||
|
||||
When a new camera (ie, one we can't match by-path to a deserialized CameraConfiguration) is activated, we'll create and spin up a new Vision Module for it.
|
||||
|
||||

|
||||
|
||||
# Camera Matching Requirements
|
||||
|
||||
## Definitions
|
||||
- VALID USB PATH: a path in the form `/dev/v4l/by-path/[UUID]`
|
||||
- VIDEO DEVICE PATH: a CSCore-provided identifier derived from the V4L path `/dev/video[N]` on Linux, or an opaque string on Windows
|
||||
- UNIQUE NAME: an identifier that is unique within the set of all deserialized CameraConfigurations and unmatched USB cameras
|
||||
- I don't love this, it means that a USB camera matched to a VisionModule will share a UNIQUE NAME, right?
|
||||
- DESERIALIZED CAMERA CONFIGURATIONS: The set of camera configurations loaded from disk and provided to the VisionSourceManager. This configuration data structure includes the UNIQUE NAME
|
||||
- CURRENTLY ACTIVE CAMERAS: The set of VisionModules currently active and processing vision data, and associated metadata
|
||||
|
||||
## Startup:
|
||||
|
||||
- GIVEN An emtpy set of deserialized Camera Configurations
|
||||
<br>WHEN PhotonVision starts
|
||||
<br>THEN no VisionModules will be started
|
||||
|
||||
- GIVEN A valid set of deserialized Camera Configurations
|
||||
<br>WHEN PhotonVision starts
|
||||
<br>THEN VisionModules will be started FOR EACH un-DISABLED config
|
||||
|
||||
- GIVEN A valid set of deserialized Camera Configurations
|
||||
<br>WHEN PhotonVision starts
|
||||
<br>THEN VisionModules will NOT be started FOR EACH DISABLED config
|
||||
|
||||
- GIVEN A CameraConfiguration with a VALID USB PATH
|
||||
<br>WHEN a VisionModule is created
|
||||
<br>THEN The VisionModule shall open the camera using the USB path
|
||||
|
||||
- GIVEN A CameraConfiguration without a valid USB path
|
||||
<br>WHEN a VisionModule is created
|
||||
<br>THEN The VisionModule shall open the camera using the VIDEO DEVICE PATH
|
||||
|
||||
## Camera (re)enumeration:
|
||||
|
||||
- GIVEN a NEW USB CAMERA is avaliable for enumeration
|
||||
<br>WHEN a USB camera is discovered by VisionSourceManager
|
||||
<br>AND the USB camera's VIDEO DEVICE PATH is not in the set of DESERIALIZED CAMERA CONFIGURATIONS
|
||||
<br>THEN a UNIQUE NAME will be assigned to the camera info
|
||||
|
||||
- GIVEN a NEW USB CAMERA is avaliable for enumeration
|
||||
<br>WHEN a USB camera is discovered by VisionSourceManager
|
||||
<br>AND the USB camera's VIDEO DEVICE PATH is in the set of DESERIALIZED CAMERA CONFIGURATIONS
|
||||
<br>THEN a UNIQUE NAME equal to the matching DESERIALIZED CAMERA CONFIGURATION will be assigned to the camera info
|
||||
- This is a weird case. How -should- we handle this? see above
|
||||
|
||||
## Creating from a new camera
|
||||
|
||||
- Given: A UNIQUE NAME from a NEW USB CAMERA
|
||||
<br>WHEN I request a new VisionModule is created for this NEW USB CAMREA
|
||||
<br>AND the camera has a VALID USB PATH
|
||||
<br>AND the camera's VALID USB PATH is not in use by any CURRENTLY ACTIVE CAMERAS
|
||||
<br>THEN a NEW VisionModule will be started for the NEW USB CAMERA using the VALID USB PATH
|
||||
|
||||
- Given: A UNIQUE NAME from a NEW USB CAMERA
|
||||
<br>WHEN I request a new VisionModule is created for this NEW USB CAMREA
|
||||
<br>AND the camera does not have a VALID USB PATH
|
||||
<br>AND the camera's VIDEO DEVICE PATH is not in use by any CURRENTLY ACTIVE CAMERAS
|
||||
<br>THEN a NEW VisionModule will be started for the NEW USB CAMERA using the VIDEO DEVICE PATH
|
||||
|
||||
## Deactivate
|
||||
|
||||
- Given: A UNIQUE NAME from a CURRENTLY ACTIVE CAMERA
|
||||
<br>WHEN I request the VisionModule be DEACTIVATED
|
||||
<br>THEN the VisionModule will be stopped for the given CURRENTLY ACTIVE CAMERA
|
||||
<br>AND the CameraConfiguration DISABLED flag will be set to TRUE
|
||||
|
||||
## Reactivate
|
||||
|
||||
- Given: A UNIQUE NAME from a DESERIALIZED CAMERA CONFIGURATIONS
|
||||
<br>WHEN I request the VisionModule be ACTIVATED
|
||||
<br>AND the CameraConfiguration's DISABLED flag is TRUE
|
||||
<br>THEN a VisionModule will be created and started for the camera
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 215 KiB |
@@ -4,4 +4,5 @@
|
||||
:maxdepth: 1
|
||||
image-rotation
|
||||
time-sync
|
||||
camera-matching
|
||||
```
|
||||
|
||||
24
photon-client/package-lock.json
generated
24
photon-client/package-lock.json
generated
@@ -13,11 +13,13 @@
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"axios": "^1.6.3",
|
||||
"jspdf": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.4",
|
||||
"three": "^0.160.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-virtual-scroll-list": "^2.3.5",
|
||||
"vue2-helpers": "^2.1.1",
|
||||
"vuetify": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -3482,8 +3484,7 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
@@ -5347,6 +5348,25 @@
|
||||
"resolved": "https://registry.npmjs.org/vue-virtual-scroll-list/-/vue-virtual-scroll-list-2.3.5.tgz",
|
||||
"integrity": "sha512-YFK6u5yltqtAOfTBcij/KGAS2SoZvzbNIAf9qTULauPObEp53xj22tDuohrrM2vNkgoD5kejXICIUBt2Q4ZDqQ=="
|
||||
},
|
||||
"node_modules/vue2-helpers": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vue2-helpers/-/vue2-helpers-2.1.1.tgz",
|
||||
"integrity": "sha512-ujYiQ5xfO8qKP3ly8hMqtNA/QGoJCTmKdYErsL3Oxr3nURJ5axah3IV4ztC/Y3zR6qsST0yVwuG1nEneQ9jXQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"vue": "~2.7.0",
|
||||
"vue-router": "^3",
|
||||
"vuex": "^3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vue-router": {
|
||||
"optional": true
|
||||
},
|
||||
"vuex": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.7.1.tgz",
|
||||
|
||||
@@ -20,11 +20,13 @@
|
||||
"@msgpack/msgpack": "^3.0.0-beta2",
|
||||
"axios": "^1.6.3",
|
||||
"jspdf": "^2.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"pinia": "^2.1.4",
|
||||
"three": "^0.160.0",
|
||||
"vue": "^2.7.14",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-virtual-scroll-list": "^2.3.5",
|
||||
"vue2-helpers": "^2.1.1",
|
||||
"vuetify": "^2.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -40,6 +40,9 @@ if (!is_demo) {
|
||||
if (data.calibrationData !== undefined) {
|
||||
useStateStore().updateCalibrationStateValuesFromWebsocket(data.calibrationData);
|
||||
}
|
||||
if (data.visionSourceManager !== undefined) {
|
||||
useStateStore().updateDiscoveredCameras(data.visionSourceManager);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
useStateStore().$patch({ backendConnected: false });
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
$default-font: "Prompt", sans-serif !default;
|
||||
$body-font-family: $default-font;
|
||||
$heading-font-family: $default-font;
|
||||
$body-background: #282c34;
|
||||
|
||||
body {
|
||||
background: $body-background;
|
||||
}
|
||||
|
||||
.v-application {
|
||||
font-family: $default-font !important;
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref, onBeforeUnmount } from "vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import loadingImage from "@/assets/images/loading.svg";
|
||||
import type { StyleValue } from "vue/types/jsx";
|
||||
import PvIcon from "@/components/common/pv-icon.vue";
|
||||
import type { UiCameraConfiguration } from "@/types/SettingTypes";
|
||||
|
||||
const props = defineProps<{
|
||||
streamType: "Raw" | "Processed";
|
||||
id: string;
|
||||
cameraSettings: UiCameraConfiguration;
|
||||
}>();
|
||||
|
||||
const emptyStreamSrc = "//:0";
|
||||
const streamSrc = computed<string>(() => {
|
||||
const port =
|
||||
useCameraSettingsStore().currentCameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
|
||||
const port = props.cameraSettings.stream[props.streamType === "Raw" ? "inputPort" : "outputPort"];
|
||||
|
||||
if (!useStateStore().backendConnected || port === 0) {
|
||||
return emptyStreamSrc;
|
||||
@@ -32,8 +32,12 @@ const streamStyle = computed<StyleValue>(() => {
|
||||
});
|
||||
|
||||
const containerStyle = computed<StyleValue>(() => {
|
||||
const resolution = useCameraSettingsStore().currentVideoFormat.resolution;
|
||||
const rotation = useCameraSettingsStore().currentPipelineSettings.inputImageRotationMode;
|
||||
if (props.cameraSettings.validVideoFormats.length === 0) {
|
||||
return { aspectRatio: "1/1" };
|
||||
}
|
||||
const resolution =
|
||||
props.cameraSettings.validVideoFormats[props.cameraSettings.pipelineSettings.cameraVideoModeIndex].resolution;
|
||||
const rotation = props.cameraSettings.pipelineSettings.inputImageRotationMode;
|
||||
if (rotation === 1 || rotation === 3) {
|
||||
return {
|
||||
aspectRatio: `${resolution.height}/${resolution.width}`
|
||||
@@ -54,9 +58,9 @@ const overlayStyle = computed<StyleValue>(() => {
|
||||
|
||||
const handleCaptureClick = () => {
|
||||
if (props.streamType === "Raw") {
|
||||
useCameraSettingsStore().saveInputSnapshot();
|
||||
props.cameraSettings.pipelineSettings[props.cameraSettings.currentPipelineIndex].saveInputSnapshot();
|
||||
} else {
|
||||
useCameraSettingsStore().saveOutputSnapshot();
|
||||
props.cameraSettings.pipelineSettings[props.cameraSettings.currentPipelineIndex].saveOutputSnapshot();
|
||||
}
|
||||
};
|
||||
const handlePopoutClick = () => {
|
||||
@@ -69,6 +73,16 @@ const handleFullscreenRequest = () => {
|
||||
};
|
||||
|
||||
const mjpgStream: any = ref(null);
|
||||
|
||||
const handleStreamError = () => {
|
||||
if (streamSrc.value && streamSrc.value !== emptyStreamSrc) {
|
||||
console.error("Error loading stream:", streamSrc.value, " Trying again.");
|
||||
setTimeout(() => {
|
||||
mjpgStream.value.src = streamSrc.value;
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (!mjpgStream.value) return;
|
||||
mjpgStream.value["src"] = emptyStreamSrc;
|
||||
@@ -79,7 +93,6 @@ onBeforeUnmount(() => {
|
||||
<div class="stream-container" :style="containerStyle">
|
||||
<img :src="loadingImage" class="stream-loading" />
|
||||
<img
|
||||
v-show="streamSrc !== emptyStreamSrc"
|
||||
:id="id"
|
||||
ref="mjpgStream"
|
||||
class="stream-video"
|
||||
@@ -87,6 +100,7 @@ onBeforeUnmount(() => {
|
||||
:src="streamSrc"
|
||||
:alt="streamDesc"
|
||||
:style="streamStyle"
|
||||
@error="handleStreamError"
|
||||
/>
|
||||
<div class="stream-overlay" :style="overlayStyle">
|
||||
<pv-icon
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import { computed, getCurrentInstance } from "vue";
|
||||
import { useSettingsStore } from "@/stores/settings/GeneralSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
|
||||
import { useRoute } from "vue2-helpers/vue-router";
|
||||
|
||||
const compact = computed<boolean>({
|
||||
get: () => {
|
||||
@@ -14,6 +17,12 @@ const compact = computed<boolean>({
|
||||
|
||||
// Vuetify2 doesn't yet support the useDisplay API so this is required to access the prop when using the Composition API
|
||||
const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.breakpoint.mdAndUp || false);
|
||||
|
||||
const needsCamerasConfigured = computed<boolean>(() => {
|
||||
return (
|
||||
useCameraSettingsStore().cameras.length === 0 || useCameraSettingsStore().cameras[0] === PlaceholderCameraSettings
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -35,14 +44,6 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
<v-list-item-title>Dashboard</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Cameras</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/settings">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
@@ -51,6 +52,26 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
<v-list-item-title>Settings</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item ref="camerasTabOpener" link to="/cameras">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-camera</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Camera</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
link
|
||||
to="/cameraConfigs"
|
||||
:class="{ cameraicon: needsCamerasConfigured && useRoute().path !== '/cameraConfigs' }"
|
||||
>
|
||||
<v-list-item-icon>
|
||||
<v-icon :class="{ 'red--text': needsCamerasConfigured }">mdi-swap-horizontal-bold</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title :class="{ 'red--text': needsCamerasConfigured }">Camera Matching</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-list-item link to="/docs">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-bookshelf</v-icon>
|
||||
@@ -119,4 +140,18 @@ const mdAndUp = computed<boolean>(() => getCurrentInstance()?.proxy.$vuetify.bre
|
||||
height: 70px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.cameraicon {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,6 +20,7 @@ const settingsValid = ref(true);
|
||||
|
||||
const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
|
||||
const uniqueResolutions: VideoFormat[] = [];
|
||||
if (useCameraSettingsStore().currentCameraSettings.validVideoFormats.length === 0) return uniqueResolutions;
|
||||
useCameraSettingsStore().currentCameraSettings.validVideoFormats.forEach((format) => {
|
||||
const index = uniqueResolutions.findIndex((v) => resolutionsAreEqual(v.resolution, format.resolution));
|
||||
const contains = index != -1;
|
||||
@@ -248,7 +249,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
|
||||
</v-simple-table>
|
||||
</v-row>
|
||||
<v-divider />
|
||||
<v-row style="display: flex; flex-direction: column" class="mt-4">
|
||||
<v-row v-if="useCameraSettingsStore().isConnected" style="display: flex; flex-direction: column" class="mt-4">
|
||||
<v-card-subtitle v-show="!isCalibrating" class="pl-3 pa-0 ma-0"> Configure New Calibration</v-card-subtitle>
|
||||
<v-form ref="form" v-model="settingsValid" class="pl-4 mb-10 pr-5">
|
||||
<!-- TODO: the default videoFormatIndex is 0, but the list of unique video mode indexes might not include 0. getUniqueVideoResolutionStrings indexing is also different from the normal video mode indexing -->
|
||||
|
||||
@@ -57,6 +57,7 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
</div>
|
||||
<div>
|
||||
<v-chip
|
||||
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
@@ -67,6 +68,9 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
{{ Math.min(Math.round(useStateStore().currentPipelineResults?.latency || 0), 9999) }} ms latency
|
||||
</span>
|
||||
</v-chip>
|
||||
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<span class="pr-1"> Camera not connected </span>
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -86,6 +90,7 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
<photon-camera-stream
|
||||
v-if="value.includes(0)"
|
||||
id="input-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
stream-type="Raw"
|
||||
style="max-width: 100%"
|
||||
/>
|
||||
@@ -94,6 +99,7 @@ const fpsTooLow = computed<boolean>(() => {
|
||||
<photon-camera-stream
|
||||
v-if="value.includes(1)"
|
||||
id="output-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
stream-type="Processed"
|
||||
style="max-width: 100%"
|
||||
/>
|
||||
|
||||
75
photon-client/src/components/common/pv-camera-info-card.vue
Normal file
75
photon-client/src/components/common/pv-camera-info-card.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
|
||||
const { camera, showTitle } = defineProps({
|
||||
camera: {
|
||||
type: PVCameraInfo,
|
||||
required: true
|
||||
},
|
||||
showTitle: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
});
|
||||
|
||||
const cameraInfoFor: any = (camera: PVCameraInfo) => {
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
}
|
||||
if (camera.PVCSICameraInfo) {
|
||||
return camera.PVCSICameraInfo;
|
||||
}
|
||||
if (camera.PVFileCameraInfo) {
|
||||
return camera.PVFileCameraInfo;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="showTitle === true">
|
||||
<h3 v-if="camera.PVUsbCameraInfo" class="mb-3">USB Camera Info</h3>
|
||||
<h3 v-if="camera.PVCSICameraInfo" class="mb-3">CSI Camera Info</h3>
|
||||
<h3 v-if="camera.PVFileCameraInfo" class="mb-3">File Camera Info</h3>
|
||||
</div>
|
||||
|
||||
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<tbody>
|
||||
<tr v-if="cameraInfoFor(camera).dev !== undefined && cameraInfoFor(camera).dev !== null">
|
||||
<td>Device Number:</td>
|
||||
<td>{{ cameraInfoFor(camera).dev }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).name !== undefined && cameraInfoFor(camera).name !== null">
|
||||
<td>Name:</td>
|
||||
<td>{{ cameraInfoFor(camera).name }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).baseName !== undefined && cameraInfoFor(camera).baseName !== null">
|
||||
<td>Base Name:</td>
|
||||
<td>{{ cameraInfoFor(camera).baseName }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).vendorId !== undefined && cameraInfoFor(camera).vendorId !== null">
|
||||
<td>Vendor ID:</td>
|
||||
<td>{{ cameraInfoFor(camera).vendorId }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).productId !== undefined && cameraInfoFor(camera).productId !== null">
|
||||
<td>Product ID:</td>
|
||||
<td>{{ cameraInfoFor(camera).productId }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).path !== undefined && cameraInfoFor(camera).path !== null">
|
||||
<td>Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(camera).path }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).otherPaths !== undefined && cameraInfoFor(camera).otherPaths !== null">
|
||||
<td>Other Paths:</td>
|
||||
<td>{{ cameraInfoFor(camera).otherPaths }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(camera).uniquePath !== undefined && cameraInfoFor(camera).uniquePath !== null">
|
||||
<td>Unique Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(camera).uniquePath }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</div>
|
||||
</template>
|
||||
85
photon-client/src/components/common/pv-camera-match-card.vue
Normal file
85
photon-client/src/components/common/pv-camera-match-card.vue
Normal file
@@ -0,0 +1,85 @@
|
||||
<script setup lang="ts">
|
||||
import { PVCameraInfo } from "@/types/SettingTypes";
|
||||
|
||||
const { saved, matched } = defineProps({
|
||||
saved: {
|
||||
type: PVCameraInfo,
|
||||
required: true
|
||||
},
|
||||
matched: {
|
||||
type: PVCameraInfo,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const cameraInfoFor = (camera: PVCameraInfo): any => {
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
}
|
||||
if (camera.PVCSICameraInfo) {
|
||||
return camera.PVCSICameraInfo;
|
||||
}
|
||||
if (camera.PVFileCameraInfo) {
|
||||
return camera.PVFileCameraInfo;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h3 v-if="saved.PVUsbCameraInfo" class="mb-3">USB Camera Info</h3>
|
||||
<h3 v-if="saved.PVCSICameraInfo" class="mb-3">CSI Camera Info</h3>
|
||||
<h3 v-if="saved.PVFileCameraInfo" class="mb-3">File Camera Info</h3>
|
||||
|
||||
<v-simple-table dense :style="{ backgroundColor: 'var(--v-primary-base)' }">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Saved</th>
|
||||
<th>Matched</th>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(saved).dev !== undefined && cameraInfoFor(saved).dev !== null">
|
||||
<td>Device Number:</td>
|
||||
<td>{{ cameraInfoFor(saved).dev }}</td>
|
||||
<td>{{ cameraInfoFor(matched).dev }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(saved).name !== undefined && cameraInfoFor(saved).name !== null">
|
||||
<td>Name:</td>
|
||||
<td>{{ cameraInfoFor(saved).name }}</td>
|
||||
<td>{{ cameraInfoFor(matched).name }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(saved).baseName !== undefined && cameraInfoFor(saved).baseName !== null">
|
||||
<td>Base Name:</td>
|
||||
<td>{{ cameraInfoFor(saved).baseName }}</td>
|
||||
<td>{{ cameraInfoFor(matched).baseName }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(saved).vendorId !== undefined && cameraInfoFor(saved).vendorId !== null">
|
||||
<td>Vendor ID:</td>
|
||||
<td>{{ cameraInfoFor(saved).vendorId }}</td>
|
||||
<td>{{ cameraInfoFor(matched).vendorId }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(saved).productId !== undefined && cameraInfoFor(saved).productId !== null">
|
||||
<td>Product ID:</td>
|
||||
<td>{{ cameraInfoFor(saved).productId }}</td>
|
||||
<td>{{ cameraInfoFor(matched).productId }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(saved).path !== undefined && cameraInfoFor(saved).path !== null">
|
||||
<td>Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(saved).path }}</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(matched).path }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(saved).otherPaths !== undefined && cameraInfoFor(saved).otherPaths !== null">
|
||||
<td>Other Paths:</td>
|
||||
<td>{{ cameraInfoFor(saved).otherPaths }}</td>
|
||||
<td>{{ cameraInfoFor(matched).otherPaths }}</td>
|
||||
</tr>
|
||||
<tr v-if="cameraInfoFor(saved).uniquePath !== undefined && cameraInfoFor(saved).uniquePath !== null">
|
||||
<td>Unique Path:</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(saved).uniquePath }}</td>
|
||||
<td style="word-break: break-all">{{ cameraInfoFor(matched).uniquePath }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -81,7 +81,7 @@ const localValue = computed({
|
||||
type="number"
|
||||
style="width: 45px"
|
||||
:step="step"
|
||||
hide-spin-buttons="true"
|
||||
:hide-spin-buttons="true"
|
||||
@keyup.enter="localValue = $event.target.value"
|
||||
@blur="localValue = $event.target.value"
|
||||
/>
|
||||
|
||||
@@ -14,7 +14,8 @@ const props = withDefaults(
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
labelCols: 2
|
||||
labelCols: 2,
|
||||
switchCols: 8
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -282,7 +282,11 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
:value="useCameraSettingsStore().currentCameraSettings.currentPipelineIndex"
|
||||
label="Pipeline"
|
||||
tooltip="Each pipeline runs on a camera output and stores a unique set of processing settings"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
:items="pipelineNamesWrapper"
|
||||
@input="(args) => useCameraSettingsStore().changeCurrentPipelineIndex(args, true)"
|
||||
/>
|
||||
@@ -349,7 +353,11 @@ useCameraSettingsStore().$subscribe((mutation, state) => {
|
||||
v-model="currentPipelineType"
|
||||
label="Type"
|
||||
tooltip="Changes the pipeline type, which changes the type of processing that will happen on input frames"
|
||||
:disabled="useCameraSettingsStore().isDriverMode || useCameraSettingsStore().isCalibrationMode"
|
||||
:disabled="
|
||||
useCameraSettingsStore().isDriverMode ||
|
||||
useCameraSettingsStore().isCalibrationMode ||
|
||||
!useCameraSettingsStore().hasConnected
|
||||
"
|
||||
:items="pipelineTypesWrapper"
|
||||
@input="showPipelineTypeChangeDialog = true"
|
||||
/>
|
||||
|
||||
@@ -49,6 +49,7 @@ const performanceRecommendation = computed<string>(() => {
|
||||
</v-col>
|
||||
<v-col class="align-self-center" style="text-align: right; margin-right: 12px; padding-left: 24px">
|
||||
<v-chip
|
||||
v-if="useCameraSettingsStore().currentCameraSettings.isConnected"
|
||||
label
|
||||
:color="fpsTooLow ? 'error' : 'transparent'"
|
||||
:text-color="fpsTooLow ? '#C7EA46' : '#ff4d00'"
|
||||
@@ -58,6 +59,9 @@ const performanceRecommendation = computed<string>(() => {
|
||||
>Processing @ {{ Math.round(useStateStore().currentPipelineResults?.fps || 0) }} FPS –</span
|
||||
><span>{{ performanceRecommendation }}</span>
|
||||
</v-chip>
|
||||
<v-chip v-else label color="transparent" text-color="red" style="font-size: 1rem; padding: 0; margin: 0">
|
||||
<span class="pr-1"> Camera not connected </span>
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col
|
||||
class="align-self-center"
|
||||
@@ -82,10 +86,20 @@ const performanceRecommendation = computed<string>(() => {
|
||||
<v-divider style="border-color: white" />
|
||||
<v-row class="stream-viewer-container pa-3">
|
||||
<v-col v-if="value.includes(0)" class="stream-view">
|
||||
<photon-camera-stream id="input-camera-stream" stream-type="Raw" style="width: 100%; height: auto" />
|
||||
<photon-camera-stream
|
||||
id="input-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
stream-type="Raw"
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col v-if="value.includes(1)" class="stream-view">
|
||||
<photon-camera-stream id="output-camera-stream" stream-type="Processed" style="width: 100%; height: auto" />
|
||||
<photon-camera-stream
|
||||
id="output-camera-stream"
|
||||
:camera-settings="useCameraSettingsStore().currentCameraSettings"
|
||||
stream-type="Processed"
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
|
||||
@@ -145,31 +145,42 @@ onBeforeUpdate(() => {
|
||||
|
||||
<template>
|
||||
<v-row no-gutters class="tabGroups">
|
||||
<v-col
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
|
||||
>
|
||||
<v-card color="primary" height="100%" class="pr-4 pl-4">
|
||||
<v-tabs
|
||||
v-model="selectedTabs[tabGroupIndex]"
|
||||
grow
|
||||
background-color="primary"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="accent"
|
||||
>
|
||||
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
|
||||
{{ tabConfig.tabName }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div class="pl-4 pr-4 pt-4 pb-2">
|
||||
<KeepAlive>
|
||||
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<template v-if="!useCameraSettingsStore().hasConnected">
|
||||
<v-col v-if="!useCameraSettingsStore().hasConnected" cols="12">
|
||||
<v-card color="error">
|
||||
<v-card-title class="white--text">
|
||||
Camera has not connected. Please check your connection and try again.
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-col
|
||||
v-for="(tabGroupData, tabGroupIndex) in tabGroups"
|
||||
:key="tabGroupIndex"
|
||||
:class="tabGroupIndex !== tabGroups.length - 1 && 'pr-3'"
|
||||
>
|
||||
<v-card color="primary" height="100%" class="pr-4 pl-4">
|
||||
<v-tabs
|
||||
v-model="selectedTabs[tabGroupIndex]"
|
||||
grow
|
||||
background-color="primary"
|
||||
dark
|
||||
height="48"
|
||||
slider-color="accent"
|
||||
>
|
||||
<v-tab v-for="(tabConfig, index) in tabGroupData" :key="index">
|
||||
{{ tabConfig.tabName }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div class="pl-4 pr-4 pt-4 pb-2">
|
||||
<KeepAlive>
|
||||
<Component :is="tabGroupData[selectedTabs[tabGroupIndex]].component" />
|
||||
</KeepAlive>
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -38,11 +38,16 @@ const processingMode = computed<number>({
|
||||
<v-col>
|
||||
<p style="color: white">Processing Mode</p>
|
||||
<v-btn-toggle v-model="processingMode" mandatory dark class="fill">
|
||||
<v-btn color="secondary">
|
||||
<v-btn color="secondary" :disabled="!useCameraSettingsStore().hasConnected">
|
||||
<v-icon left>mdi-square-outline</v-icon>
|
||||
<span>2D</span>
|
||||
</v-btn>
|
||||
<v-btn color="secondary" :disabled="!useCameraSettingsStore().isCurrentVideoFormatCalibrated">
|
||||
<v-btn
|
||||
color="secondary"
|
||||
:disabled="
|
||||
!useCameraSettingsStore().hasConnected || !useCameraSettingsStore().isCurrentVideoFormatCalibrated
|
||||
"
|
||||
>
|
||||
<v-icon left>mdi-cube-outline</v-icon>
|
||||
<span>3D</span>
|
||||
</v-btn>
|
||||
|
||||
@@ -6,6 +6,7 @@ import CameraSettingsView from "@/views/CameraSettingsView.vue";
|
||||
import GeneralSettingsView from "@/views/GeneralSettingsView.vue";
|
||||
import DocsView from "@/views/DocsView.vue";
|
||||
import NotFoundView from "@/views/NotFoundView.vue";
|
||||
import CameraMatchingView from "@/views/CameraMatchingView.vue";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
@@ -33,6 +34,11 @@ const router = new VueRouter({
|
||||
name: "Settings",
|
||||
component: GeneralSettingsView
|
||||
},
|
||||
{
|
||||
path: "/cameraConfigs",
|
||||
name: "Camera Matching",
|
||||
component: CameraMatchingView
|
||||
},
|
||||
{
|
||||
path: "/docs",
|
||||
name: "Docs",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { LogMessage } from "@/types/SettingTypes";
|
||||
import type { LogMessage, VsmState } from "@/types/SettingTypes";
|
||||
import type { AutoReconnectingWebsocket } from "@/lib/AutoReconnectingWebsocket";
|
||||
import type { MultitagResult, PipelineResult } from "@/types/PhotonTrackingTypes";
|
||||
import type {
|
||||
@@ -24,7 +24,7 @@ interface StateStore {
|
||||
logMessages: LogMessage[];
|
||||
currentCameraIndex: number;
|
||||
|
||||
backendResults: Record<string, PipelineResult>;
|
||||
backendResults: Record<number, PipelineResult>;
|
||||
multitagResultBuffer: Record<string, MultitagResult[]>;
|
||||
|
||||
colorPickingMode: boolean;
|
||||
@@ -42,6 +42,8 @@ interface StateStore {
|
||||
color: string;
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
vsmState: VsmState;
|
||||
}
|
||||
|
||||
export const useStateStore = defineStore("state", {
|
||||
@@ -59,7 +61,16 @@ export const useStateStore = defineStore("state", {
|
||||
logMessages: [],
|
||||
currentCameraIndex: 0,
|
||||
|
||||
backendResults: {},
|
||||
backendResults: {
|
||||
0: {
|
||||
classNames: [],
|
||||
fps: 1,
|
||||
latency: 2,
|
||||
sequenceID: 3,
|
||||
targets: [],
|
||||
multitagResult: undefined
|
||||
}
|
||||
},
|
||||
multitagResultBuffer: {},
|
||||
|
||||
colorPickingMode: false,
|
||||
@@ -76,6 +87,11 @@ export const useStateStore = defineStore("state", {
|
||||
message: "No Message",
|
||||
color: "info",
|
||||
timeout: 2000
|
||||
},
|
||||
|
||||
vsmState: {
|
||||
allConnectedCameras: [],
|
||||
disabledConfigs: []
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -136,6 +152,9 @@ export const useStateStore = defineStore("state", {
|
||||
hasEnoughImages: data.hasEnough
|
||||
};
|
||||
},
|
||||
updateDiscoveredCameras(data: VsmState) {
|
||||
this.vsmState = data;
|
||||
},
|
||||
showSnackbarMessage(data: { message: string; color: string; timeout?: number }) {
|
||||
this.snackbarData = {
|
||||
show: true,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
CalibrationTagFamilies,
|
||||
CalibrationBoardTypes,
|
||||
CameraCalibrationResult,
|
||||
CameraSettings,
|
||||
UiCameraConfiguration,
|
||||
CameraSettingsChangeRequest,
|
||||
Resolution,
|
||||
RobotOffsetType,
|
||||
@@ -18,7 +18,7 @@ import axios from "axios";
|
||||
import { resolutionsAreEqual } from "@/lib/PhotonUtils";
|
||||
|
||||
interface CameraSettingsStore {
|
||||
cameras: CameraSettings[];
|
||||
cameras: UiCameraConfiguration[];
|
||||
}
|
||||
|
||||
export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
@@ -27,7 +27,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
}),
|
||||
getters: {
|
||||
// TODO update types to update this value being undefined. This would be a decently large change.
|
||||
currentCameraSettings(): CameraSettings {
|
||||
currentCameraSettings(): UiCameraConfiguration {
|
||||
return this.cameras[useStateStore().currentCameraIndex];
|
||||
},
|
||||
currentPipelineSettings(): ActivePipelineSettings {
|
||||
@@ -52,7 +52,7 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
return this.cameras.map((c) => c.nickname);
|
||||
},
|
||||
cameraUniqueNames(): string[] {
|
||||
return this.cameras.map((c) => c.nickname);
|
||||
return this.cameras.map((c) => c.uniqueName);
|
||||
},
|
||||
currentCameraName(): string {
|
||||
return this.cameraNames[useStateStore().currentCameraIndex];
|
||||
@@ -83,11 +83,19 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
},
|
||||
maxWhiteBalanceTemp(): number {
|
||||
return this.currentCameraSettings.maxWhiteBalanceTemp;
|
||||
},
|
||||
isConnected(): boolean {
|
||||
return this.currentCameraSettings.isConnected;
|
||||
},
|
||||
hasConnected(): boolean {
|
||||
return this.currentCameraSettings.hasConnected;
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
updateCameraSettingsFromWebsocket(data: WebsocketCameraSettingsUpdate[]) {
|
||||
const configuredCameras = data.map<CameraSettings>((d) => ({
|
||||
const configuredCameras = data.map<UiCameraConfiguration>((d) => ({
|
||||
cameraPath: d.cameraPath,
|
||||
|
||||
nickname: d.nickname,
|
||||
uniqueName: d.uniqueName,
|
||||
fov: {
|
||||
@@ -124,8 +132,18 @@ export const useCameraSettingsStore = defineStore("cameraSettings", {
|
||||
pipelineSettings: d.currentPipelineSettings,
|
||||
cameraQuirks: d.cameraQuirks,
|
||||
minWhiteBalanceTemp: d.minWhiteBalanceTemp,
|
||||
maxWhiteBalanceTemp: d.maxWhiteBalanceTemp
|
||||
maxWhiteBalanceTemp: d.maxWhiteBalanceTemp,
|
||||
matchedCameraInfo: d.matchedCameraInfo,
|
||||
isConnected: d.isConnected,
|
||||
hasConnected: d.hasConnected
|
||||
}));
|
||||
|
||||
// Clamp index to between 0 and [length - 1]
|
||||
useStateStore().currentCameraIndex = Math.max(
|
||||
0,
|
||||
Math.min(useStateStore().currentCameraIndex, configuredCameras.length - 1)
|
||||
);
|
||||
|
||||
this.cameras = configuredCameras.length > 0 ? configuredCameras : [PlaceholderCameraSettings];
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -10,14 +10,13 @@ import { NetworkConnectionType } from "@/types/SettingTypes";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import axios from "axios";
|
||||
import type { WebsocketSettingsUpdate } from "@/types/WebsocketDataTypes";
|
||||
import type { AprilTagFieldLayout } from "@/types/PhotonTrackingTypes";
|
||||
|
||||
interface GeneralSettingsStore {
|
||||
general: GeneralSettings;
|
||||
network: NetworkSettings;
|
||||
lighting: LightingSettings;
|
||||
metrics: MetricData;
|
||||
currentFieldLayout: AprilTagFieldLayout;
|
||||
currentFieldLayout;
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore("settings", {
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface MultitagResult {
|
||||
}
|
||||
|
||||
export interface PipelineResult {
|
||||
sequenceID: number;
|
||||
fps: number;
|
||||
latency: number;
|
||||
targets: PhotonTarget[];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type ActivePipelineSettings, DefaultAprilTagPipelineSettings } from "@/types/PipelineTypes";
|
||||
import type { Pose3d } from "@/types/PhotonTrackingTypes";
|
||||
import type { WebsocketCameraSettingsUpdate } from "./WebsocketDataTypes";
|
||||
|
||||
export interface GeneralSettings {
|
||||
version?: string;
|
||||
@@ -56,6 +57,52 @@ export type ConfigurableNetworkSettings = Omit<
|
||||
"canManage" | "networkInterfaceNames" | "networkingDisabled"
|
||||
>;
|
||||
|
||||
export interface PVCameraInfoBase {
|
||||
/*
|
||||
Huge hack. In Jackson, this is set based on the underlying type -- this
|
||||
then maps to one of the 3 subclasses here below. Not sure how to best deal with this.
|
||||
*/
|
||||
cameraTypename: "PVUsbCameraInfo" | "PVCSICameraInfo" | "PVFileCameraInfo";
|
||||
}
|
||||
|
||||
export interface PVUsbCameraInfo {
|
||||
dev: number;
|
||||
name: string;
|
||||
otherPaths: string[];
|
||||
path: string;
|
||||
vendorId: number;
|
||||
productId: number;
|
||||
|
||||
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
|
||||
uniquePath: string;
|
||||
}
|
||||
export interface PVCSICameraInfo {
|
||||
baseName: string;
|
||||
path: string;
|
||||
|
||||
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
|
||||
uniquePath: string;
|
||||
}
|
||||
export interface PVFileCameraInfo {
|
||||
path: string;
|
||||
name: string;
|
||||
|
||||
// In Java, PVCameraInfo provides a uniquePath property so we can have one Source of Truth here
|
||||
uniquePath: string;
|
||||
}
|
||||
|
||||
// This camera info will only ever hold one of its members - the others should be undefined.
|
||||
export class PVCameraInfo {
|
||||
PVUsbCameraInfo: PVUsbCameraInfo | undefined;
|
||||
PVCSICameraInfo: PVCSICameraInfo | undefined;
|
||||
PVFileCameraInfo: PVFileCameraInfo | undefined;
|
||||
}
|
||||
|
||||
export interface VsmState {
|
||||
disabledConfigs: WebsocketCameraSettingsUpdate[];
|
||||
allConnectedCameras: PVCameraInfo[];
|
||||
}
|
||||
|
||||
export interface LightingSettings {
|
||||
supported: boolean;
|
||||
brightness: number;
|
||||
@@ -172,7 +219,9 @@ export interface QuirkyCamera {
|
||||
quirks: Record<ValidQuirks, boolean>;
|
||||
}
|
||||
|
||||
export interface CameraSettings {
|
||||
export interface UiCameraConfiguration {
|
||||
cameraPath: string;
|
||||
|
||||
nickname: string;
|
||||
uniqueName: string;
|
||||
|
||||
@@ -201,6 +250,10 @@ export interface CameraSettings {
|
||||
|
||||
minWhiteBalanceTemp: number;
|
||||
maxWhiteBalanceTemp: number;
|
||||
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
}
|
||||
|
||||
export interface CameraSettingsChangeRequest {
|
||||
@@ -208,7 +261,9 @@ export interface CameraSettingsChangeRequest {
|
||||
quirksToChange: Record<ValidQuirks, boolean>;
|
||||
}
|
||||
|
||||
export const PlaceholderCameraSettings: CameraSettings = {
|
||||
export const PlaceholderCameraSettings: UiCameraConfiguration = {
|
||||
cameraPath: "/dev/null",
|
||||
|
||||
nickname: "Placeholder Camera",
|
||||
uniqueName: "Placeholder Name",
|
||||
fov: {
|
||||
@@ -307,7 +362,18 @@ export const PlaceholderCameraSettings: CameraSettings = {
|
||||
minExposureRaw: 1,
|
||||
maxExposureRaw: 100,
|
||||
minWhiteBalanceTemp: 2000,
|
||||
maxWhiteBalanceTemp: 10000
|
||||
maxWhiteBalanceTemp: 10000,
|
||||
matchedCameraInfo: {
|
||||
PVFileCameraInfo: {
|
||||
name: "Foobar",
|
||||
path: "/dev/foobar",
|
||||
uniquePath: "/dev/foobar2"
|
||||
},
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
},
|
||||
isConnected: true,
|
||||
hasConnected: true
|
||||
};
|
||||
|
||||
export enum CalibrationBoardTypes {
|
||||
|
||||
@@ -5,7 +5,9 @@ import type {
|
||||
LogLevel,
|
||||
MetricData,
|
||||
NetworkSettings,
|
||||
QuirkyCamera
|
||||
PVCameraInfo,
|
||||
QuirkyCamera,
|
||||
VsmState
|
||||
} from "@/types/SettingTypes";
|
||||
import type { ActivePipelineSettings } from "@/types/PipelineTypes";
|
||||
import type { AprilTagFieldLayout, PipelineResult } from "@/types/PhotonTrackingTypes";
|
||||
@@ -44,7 +46,9 @@ export type WebsocketVideoFormat = Record<
|
||||
}
|
||||
>;
|
||||
|
||||
// Companion to UICameraConfiguration in Java
|
||||
export interface WebsocketCameraSettingsUpdate {
|
||||
cameraPath: string;
|
||||
calibrations: CameraCalibrationResult[];
|
||||
currentPipelineIndex: number;
|
||||
currentPipelineSettings: ActivePipelineSettings;
|
||||
@@ -62,6 +66,9 @@ export interface WebsocketCameraSettingsUpdate {
|
||||
maxExposureRaw: number;
|
||||
minWhiteBalanceTemp: number;
|
||||
maxWhiteBalanceTemp: number;
|
||||
matchedCameraInfo: PVCameraInfo;
|
||||
isConnected: boolean;
|
||||
hasConnected: boolean;
|
||||
}
|
||||
export interface WebsocketNTUpdate {
|
||||
connected: boolean;
|
||||
@@ -98,6 +105,7 @@ export interface IncomingWebsocketData {
|
||||
mutatePipelineSettings?: Partial<ActivePipelineSettings>;
|
||||
cameraIndex?: number; // Sent when mutating pipeline settings to check against currently active
|
||||
calibrationData?: WebsocketCalibrationData;
|
||||
visionSourceManager?: VsmState;
|
||||
}
|
||||
|
||||
export enum WebsocketPipelineType {
|
||||
|
||||
445
photon-client/src/views/CameraMatchingView.vue
Normal file
445
photon-client/src/views/CameraMatchingView.vue
Normal file
@@ -0,0 +1,445 @@
|
||||
<script setup lang="ts">
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import {
|
||||
PlaceholderCameraSettings,
|
||||
PVCameraInfo,
|
||||
type PVCSICameraInfo,
|
||||
type PVFileCameraInfo,
|
||||
type PVUsbCameraInfo
|
||||
} from "@/types/SettingTypes";
|
||||
import { getResolutionString } from "@/lib/PhotonUtils";
|
||||
import PvCameraInfoCard from "@/components/common/pv-camera-info-card.vue";
|
||||
import axios from "axios";
|
||||
import _ from "lodash";
|
||||
|
||||
const formatUrl = (port) => `http://${inject("backendHostname")}:${port}/stream.mjpg`;
|
||||
const host = inject<string>("backendHost");
|
||||
|
||||
const activateModule = (moduleUniqueName: string) => {
|
||||
const url = new URL(`http://${host}/api/utils/activateMatchedCamera`);
|
||||
url.searchParams.set("uniqueName", moduleUniqueName);
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: "POST"
|
||||
});
|
||||
};
|
||||
const activateCamera = (cameraInfo: PVCameraInfo) => {
|
||||
const url = new URL(`http://${host}/api/utils/assignUnmatchedCamera`);
|
||||
url.searchParams.set("cameraInfo", JSON.stringify(cameraInfo));
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: "POST"
|
||||
});
|
||||
};
|
||||
const deactivateCamera = (cameraUniqueName: string) => {
|
||||
const url = new URL(`http://${host}/api/utils/unassignCamera`);
|
||||
url.searchParams.set("uniqueName", cameraUniqueName);
|
||||
|
||||
fetch(url.toString(), {
|
||||
method: "POST"
|
||||
});
|
||||
};
|
||||
|
||||
const deleteThisCamera = (cameraName: string) => {
|
||||
const payload = {
|
||||
cameraUniqueName: cameraName
|
||||
};
|
||||
|
||||
axios
|
||||
.post("/utils/nukeOneCamera", payload)
|
||||
.then(() => {
|
||||
useStateStore().showSnackbarMessage({
|
||||
message: "Successfully deleted " + cameraName,
|
||||
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"
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const cameraInfoFor = (camera: PVCameraInfo): PVUsbCameraInfo | PVCSICameraInfo | PVFileCameraInfo | any => {
|
||||
if (camera.PVUsbCameraInfo) {
|
||||
return camera.PVUsbCameraInfo;
|
||||
}
|
||||
if (camera.PVCSICameraInfo) {
|
||||
return camera.PVCSICameraInfo;
|
||||
}
|
||||
if (camera.PVFileCameraInfo) {
|
||||
return camera.PVFileCameraInfo;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const uniquePathForCamera = (info: PVCameraInfo) => {
|
||||
if (info.PVUsbCameraInfo) {
|
||||
return info.PVUsbCameraInfo.uniquePath;
|
||||
}
|
||||
if (info.PVCSICameraInfo) {
|
||||
return info.PVCSICameraInfo.uniquePath;
|
||||
}
|
||||
if (info.PVFileCameraInfo) {
|
||||
return info.PVFileCameraInfo.uniquePath;
|
||||
}
|
||||
|
||||
// TODO - wut
|
||||
return "";
|
||||
};
|
||||
|
||||
/**
|
||||
* Find the PVCameraInfo currently occupying the same uniquepath as the the given module
|
||||
*/
|
||||
const getMatchedDevice = (info: PVCameraInfo | undefined): PVCameraInfo => {
|
||||
if (!info) {
|
||||
return {
|
||||
PVFileCameraInfo: {
|
||||
name: "!",
|
||||
path: "!",
|
||||
uniquePath: "!"
|
||||
},
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
};
|
||||
}
|
||||
return (
|
||||
useStateStore().vsmState.allConnectedCameras.find(
|
||||
(it) => uniquePathForCamera(it) === uniquePathForCamera(info)
|
||||
) || {
|
||||
PVFileCameraInfo: {
|
||||
name: "!",
|
||||
path: "!",
|
||||
uniquePath: "!"
|
||||
},
|
||||
PVCSICameraInfo: undefined,
|
||||
PVUsbCameraInfo: undefined
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const unmatchedCameras = computed(() => {
|
||||
const activeVmPaths = useCameraSettingsStore().cameras.map((it) => uniquePathForCamera(it.matchedCameraInfo));
|
||||
const disabledVmPaths = useStateStore().vsmState.disabledConfigs.map((it) =>
|
||||
uniquePathForCamera(it.matchedCameraInfo)
|
||||
);
|
||||
|
||||
return useStateStore().vsmState.allConnectedCameras.filter(
|
||||
(it) => !activeVmPaths.includes(uniquePathForCamera(it)) && !disabledVmPaths.includes(uniquePathForCamera(it))
|
||||
);
|
||||
});
|
||||
|
||||
const activeVisionModules = computed(() =>
|
||||
useCameraSettingsStore().cameras.filter(
|
||||
(camera) => JSON.stringify(camera) !== JSON.stringify(PlaceholderCameraSettings)
|
||||
)
|
||||
);
|
||||
const disabledVisionModules = computed(() => useStateStore().vsmState.disabledConfigs);
|
||||
|
||||
const viewingDetails = ref(false);
|
||||
const showCurrentView = ref(false);
|
||||
const viewingCamera = ref<PVCameraInfo | null>(null);
|
||||
|
||||
const setCameraView = (camera: PVCameraInfo | null, showCurrent: boolean = false) => {
|
||||
viewingDetails.value = camera !== null;
|
||||
viewingCamera.value = camera;
|
||||
showCurrentView.value = showCurrent;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pa-5">
|
||||
<v-row>
|
||||
<!-- Active modules -->
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
lg="4"
|
||||
v-for="(module, index) in activeVisionModules"
|
||||
:key="`enabled-${module.uniqueName}`"
|
||||
>
|
||||
<v-card dark color="primary">
|
||||
<v-card-title>{{ module.nickname }}</v-card-title>
|
||||
<v-card-subtitle v-if="_.isEqual(getMatchedDevice(module.matchedCameraInfo), module.matchedCameraInfo)"
|
||||
>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>
|
||||
<v-simple-table dark dense>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Streams:</td>
|
||||
<td>
|
||||
<a :href="formatUrl(module.stream.inputPort)" target="_blank" class="active-status">
|
||||
Input Stream
|
||||
</a>
|
||||
/
|
||||
<a :href="formatUrl(module.stream.outputPort)" target="_blank" class="active-status">
|
||||
Output Stream
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Pipelines</td>
|
||||
<td>{{ module.pipelineNicknames.join(", ") }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Connected</td>
|
||||
<td>{{ module.isConnected }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Calibrations</td>
|
||||
<td>
|
||||
{{
|
||||
module.completeCalibrations.map((it) => getResolutionString(it.resolution)).join(", ") ||
|
||||
"Not calibrated"
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="module.isConnected && useStateStore().backendResults[index]">
|
||||
<td>Frames Processed</td>
|
||||
<td>
|
||||
{{ useStateStore().backendResults[index].sequenceID }} ({{
|
||||
useStateStore().backendResults[index].fps
|
||||
}}
|
||||
FPS)
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
<photon-camera-stream
|
||||
class="mt-3"
|
||||
id="output-camera-stream"
|
||||
:camera-settings="module"
|
||||
stream-type="Processed"
|
||||
style="width: 100%; height: auto"
|
||||
/>
|
||||
</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="secondary" @click="setCameraView(module.matchedCameraInfo, true)" style="width: 100%">
|
||||
<span>Details</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="5" class="pr-0">
|
||||
<v-btn
|
||||
class="black--text"
|
||||
@click="deactivateCamera(module.uniqueName)"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
>
|
||||
Deactivate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-btn
|
||||
class="black--text pa-0"
|
||||
@click="deleteThisCamera(module.uniqueName)"
|
||||
color="red"
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-icon>mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Disabled modules -->
|
||||
<v-col cols="12" sm="6" lg="4" v-for="module in disabledVisionModules" :key="`disabled-${module.uniqueName}`">
|
||||
<v-card dark color="primary">
|
||||
<v-card-title>{{ module.nickname }}</v-card-title>
|
||||
<v-card-subtitle>Status: <span class="inactive-status">Deactivated</span></v-card-subtitle>
|
||||
<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>
|
||||
<td>{{ module.isConnected }}</td>
|
||||
</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">
|
||||
<v-btn color="secondary" @click="setCameraView(module.matchedCameraInfo)" style="width: 100%">
|
||||
<span>Details</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="5" class="pr-0">
|
||||
<v-btn
|
||||
class="black--text"
|
||||
@click="activateModule(module.uniqueName)"
|
||||
color="accent"
|
||||
style="width: 100%"
|
||||
>
|
||||
Activate
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6" md="3">
|
||||
<v-btn
|
||||
class="black--text pa-0"
|
||||
@click="deleteThisCamera(module.uniqueName)"
|
||||
color="red"
|
||||
style="width: 100%"
|
||||
>
|
||||
<v-icon>mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Unassigned cameras -->
|
||||
<v-col cols="12" sm="6" lg="4" v-for="(camera, index) in unmatchedCameras" :key="index">
|
||||
<v-card dark color="primary">
|
||||
<v-card-title>
|
||||
<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">
|
||||
<v-btn color="secondary" @click="setCameraView(camera)" style="width: 100%">
|
||||
<span>Details</span>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<v-btn class="black--text" @click="activateCamera(camera)" color="accent" style="width: 100%">
|
||||
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 -->
|
||||
<v-dialog v-model="viewingDetails">
|
||||
<v-card dark flat color="primary" v-if="viewingCamera !== null">
|
||||
<v-card-title class="d-flex justify-space-between">
|
||||
<span>{{ cameraInfoFor(viewingCamera)?.name ?? cameraInfoFor(viewingCamera)?.baseName }}</span>
|
||||
<v-btn text @click="setCameraView(null)">
|
||||
<v-icon>mdi-close-thick</v-icon>
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-banner
|
||||
v-show="!_.isEqual(getMatchedDevice(viewingCamera), viewingCamera)"
|
||||
rounded
|
||||
color="red"
|
||||
text-color="white"
|
||||
icon="mdi-information-outline"
|
||||
class="mb-3"
|
||||
>
|
||||
Camera Mismatched:<br />It looks like a different camera has been connected to this device! Compare the
|
||||
below information carefully.
|
||||
</v-banner>
|
||||
<div v-if="showCurrentView">
|
||||
<h3>Saved camera</h3>
|
||||
<PvCameraInfoCard :camera="viewingCamera" :showTitle="false" />
|
||||
<br />
|
||||
<h3>Current camera</h3>
|
||||
<PvCameraInfoCard :camera="getMatchedDevice(viewingCamera)" :showTitle="false" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<PvCameraInfoCard :camera="viewingCamera" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.v-data-table {
|
||||
background-color: #006492 !important;
|
||||
}
|
||||
|
||||
a:link,
|
||||
.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,
|
||||
.mismatch-status {
|
||||
color: yellow;
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -6,6 +6,7 @@ import StreamConfigCard from "@/components/dashboard/StreamConfigCard.vue";
|
||||
import PipelineConfigCard from "@/components/dashboard/ConfigOptions.vue";
|
||||
import { useCameraSettingsStore } from "@/stores/settings/CameraSettingsStore";
|
||||
import { useStateStore } from "@/stores/StateStore";
|
||||
import { PlaceholderCameraSettings } from "@/types/SettingTypes";
|
||||
|
||||
const cameraViewType = computed<number[]>({
|
||||
get: (): number[] => {
|
||||
@@ -37,6 +38,13 @@ const cameraViewType = computed<number[]>({
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO - deduplicate with needsCamerasConfigured
|
||||
const warningShown = computed<boolean>(() => {
|
||||
return (
|
||||
useCameraSettingsStore().cameras.length === 0 || useCameraSettingsStore().cameras[0] === PlaceholderCameraSettings
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -51,5 +59,39 @@ const cameraViewType = computed<number[]>({
|
||||
</v-col>
|
||||
</v-row>
|
||||
<PipelineConfigCard />
|
||||
|
||||
<!-- TODO - not sure this belongs here -->
|
||||
<v-dialog :persistent="false" v-model="warningShown" v-if="warningShown" max-width="1500" dark>
|
||||
<v-card dark flat color="primary">
|
||||
<v-card-title>Setup some cameras to get started!</v-card-title>
|
||||
<v-card-text>
|
||||
No cameras activated - head to the <a href="#/cameraConfigs">Camera matching tab</a> to set some up!
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
a:link {
|
||||
color: #ffd843;
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:visited {
|
||||
color: #ffd843;
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: pink;
|
||||
background-color: transparent;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:active {
|
||||
color: yellow;
|
||||
background-color: transparent;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -20,14 +20,16 @@ package org.photonvision.common.configuration;
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.CameraType;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.pipeline.CVPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.DriverModePipelineSettings;
|
||||
@@ -36,83 +38,108 @@ import org.photonvision.vision.processes.PipelineManager;
|
||||
public class CameraConfiguration {
|
||||
private static final Logger logger = new Logger(CameraConfiguration.class, LogGroup.Camera);
|
||||
|
||||
/** Name as reported by CSCore */
|
||||
public String baseName = "";
|
||||
|
||||
/** Name used to title the subfolder of this config */
|
||||
/** A unique name (ostensibly an opaque UUID) to identify this particular configuration */
|
||||
public String uniqueName = "";
|
||||
|
||||
/**
|
||||
* The info of the camera we last matched to. We still match by unique path (where we can), but
|
||||
* this is useful to provide warnings to users
|
||||
*/
|
||||
public PVCameraInfo matchedCameraInfo;
|
||||
|
||||
/** User-set nickname */
|
||||
public String nickname = "";
|
||||
|
||||
/** Can be either path (ex /dev/videoX) or index (ex 1). */
|
||||
public String path = "";
|
||||
/** Deactivated vision modules do not open camera hardware or lock USB ports */
|
||||
public boolean deactivated = false;
|
||||
|
||||
public QuirkyCamera cameraQuirks;
|
||||
|
||||
@JsonIgnore public String[] otherPaths = {};
|
||||
|
||||
@JsonProperty("usbVID")
|
||||
public int usbVID = -1;
|
||||
|
||||
@JsonProperty("usbPID")
|
||||
public int usbPID = -1;
|
||||
|
||||
public CameraType cameraType = CameraType.UsbCamera;
|
||||
public double FOV = 70;
|
||||
public final List<CameraCalibrationCoefficients> calibrations;
|
||||
public List<CameraCalibrationCoefficients> calibrations = new ArrayList<>();
|
||||
public int currentPipelineIndex = 0;
|
||||
|
||||
public int streamIndex = 0; // 0 index means ports [1181, 1182], 1 means [1183, 1184], etc...
|
||||
|
||||
@JsonIgnore // this ignores the pipes as we serialize them to their own subfolder
|
||||
public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
|
||||
// Ignore the pipes, as we serialize them to their own column to hack around
|
||||
// polymorphic lists
|
||||
@JsonIgnore public List<CVPipelineSettings> pipelineSettings = new ArrayList<>();
|
||||
|
||||
@JsonIgnore
|
||||
public DriverModePipelineSettings driveModeSettings = new DriverModePipelineSettings();
|
||||
|
||||
public CameraConfiguration(String baseName, String path) {
|
||||
this(baseName, baseName, baseName, path, new String[0]);
|
||||
}
|
||||
|
||||
public CameraConfiguration(
|
||||
String baseName, String uniqueName, String nickname, String path, String[] alternates) {
|
||||
this.baseName = baseName;
|
||||
public CameraConfiguration(PVCameraInfo cameraInfo, String uniqueName, String nickname) {
|
||||
this.matchedCameraInfo = cameraInfo;
|
||||
this.uniqueName = uniqueName;
|
||||
this.nickname = nickname;
|
||||
this.path = path;
|
||||
this.calibrations = new ArrayList<>();
|
||||
this.otherPaths = alternates;
|
||||
|
||||
logger.debug("Creating USB camera configuration for " + this.toShortString());
|
||||
}
|
||||
|
||||
// Shiny new constructor
|
||||
@JsonCreator
|
||||
public CameraConfiguration(
|
||||
@JsonProperty("baseName") String baseName,
|
||||
@JsonProperty("uniqueName") String uniqueName,
|
||||
@JsonProperty("matchedCameraInfo") PVCameraInfo matchedCameraInfo,
|
||||
@JsonProperty("nickname") String nickname,
|
||||
@JsonProperty("FOV") double FOV,
|
||||
@JsonProperty("path") String path,
|
||||
@JsonProperty("cameraType") CameraType cameraType,
|
||||
@JsonProperty("deactivated") boolean deactivated,
|
||||
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
|
||||
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
|
||||
@JsonProperty("currentPipelineIndex") int currentPipelineIndex,
|
||||
@JsonProperty("usbVID") int usbVID,
|
||||
@JsonProperty("usbPID") int usbPID) {
|
||||
this.baseName = baseName;
|
||||
@JsonProperty("FOV") double FOV,
|
||||
@JsonProperty("calibrations") List<CameraCalibrationCoefficients> calibrations,
|
||||
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) {
|
||||
this.uniqueName = uniqueName;
|
||||
this.matchedCameraInfo = matchedCameraInfo;
|
||||
this.nickname = nickname;
|
||||
this.FOV = FOV;
|
||||
this.path = path;
|
||||
this.cameraType = cameraType;
|
||||
this.deactivated = deactivated;
|
||||
this.cameraQuirks = cameraQuirks;
|
||||
this.FOV = FOV;
|
||||
this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
|
||||
this.currentPipelineIndex = currentPipelineIndex;
|
||||
this.usbPID = usbPID;
|
||||
this.usbVID = usbVID;
|
||||
}
|
||||
|
||||
logger.debug("Loaded camera configuration for " + toShortString());
|
||||
// Special case constructor for use with File sources
|
||||
public CameraConfiguration(String uniqueName, PVCameraInfo camInfo) {
|
||||
this.uniqueName = uniqueName;
|
||||
this.matchedCameraInfo = camInfo;
|
||||
this.nickname = camInfo.humanReadableName();
|
||||
this.calibrations = new ArrayList<>();
|
||||
this.cameraQuirks = null; // we'll deal with this later. TODO: should we not just do it now?
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for when we don't know anything about the camera yet. Generates a UUID for the
|
||||
* unique name
|
||||
*/
|
||||
public CameraConfiguration(PVCameraInfo camInfo) {
|
||||
this(UUID.randomUUID().toString(), camInfo);
|
||||
}
|
||||
|
||||
public static class LegacyCameraConfigStruct {
|
||||
PVCameraInfo matchedCameraInfo;
|
||||
|
||||
/** Legacy constructor for compat with 2024.3.1 */
|
||||
@JsonCreator
|
||||
public LegacyCameraConfigStruct(
|
||||
@JsonProperty("baseName") String baseName,
|
||||
@JsonProperty("path") String path,
|
||||
@JsonProperty("otherPaths") String[] otherPaths,
|
||||
@JsonProperty("cameraType") CameraType cameraType,
|
||||
@JsonProperty("usbVID") int usbVID,
|
||||
@JsonProperty("usbPID") int usbPID) {
|
||||
if (cameraType == CameraType.UsbCamera) {
|
||||
this.matchedCameraInfo =
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
new UsbCameraInfo(-1, path, baseName, otherPaths, usbVID, usbPID));
|
||||
} else if (cameraType == CameraType.ZeroCopyPicam) {
|
||||
this.matchedCameraInfo = PVCameraInfo.fromCSICameraInfo(path, baseName);
|
||||
} else {
|
||||
// wtf
|
||||
logger.error("Camera type is invalid");
|
||||
this.matchedCameraInfo = null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void addPipelineSettings(List<CVPipelineSettings> settings) {
|
||||
@@ -163,54 +190,43 @@ public class CameraConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a unique descriptor of the USB port this camera is attached to. EG
|
||||
* "/dev/v4l/by-path/platform-fc800000.usb-usb-0:1.3:1.0-video-index0"
|
||||
* cscore will auto-reconnect to the camera path we give it. v4l does not guarantee that if i swap
|
||||
* cameras around, the same /dev/videoN ID will be assigned to that camera. So instead default to
|
||||
* pinning to a particular USB port, or by "path" (appears to be a global identifier on Windows).
|
||||
*
|
||||
* @return
|
||||
* <p>This represents our best guess at an immutable path to detect a camera at.
|
||||
*/
|
||||
@JsonIgnore
|
||||
public Optional<String> getUSBPath() {
|
||||
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
|
||||
public String getDevicePath() {
|
||||
return matchedCameraInfo.uniquePath();
|
||||
}
|
||||
|
||||
public String toShortString() {
|
||||
return "CameraConfiguration [baseName="
|
||||
+ baseName
|
||||
+ ", uniqueName="
|
||||
return "CameraConfiguration [uniqueName="
|
||||
+ uniqueName
|
||||
+ ", matchedCameraInfo="
|
||||
+ matchedCameraInfo
|
||||
+ ", nickname="
|
||||
+ nickname
|
||||
+ ", path="
|
||||
+ path
|
||||
+ ", otherPaths="
|
||||
+ Arrays.toString(otherPaths)
|
||||
+ ", cameraType="
|
||||
+ cameraType
|
||||
+ ", deactivated="
|
||||
+ deactivated
|
||||
+ ", cameraQuirks="
|
||||
+ cameraQuirks
|
||||
+ ", FOV="
|
||||
+ FOV
|
||||
+ "]"
|
||||
+ ", PID="
|
||||
+ usbPID
|
||||
+ ", VID="
|
||||
+ usbVID;
|
||||
+ "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CameraConfiguration [baseName="
|
||||
+ baseName
|
||||
+ ", uniqueName="
|
||||
return "CameraConfiguration [uniqueName="
|
||||
+ uniqueName
|
||||
+ ", matchedCameraInfo="
|
||||
+ matchedCameraInfo
|
||||
+ ", nickname="
|
||||
+ nickname
|
||||
+ ", path="
|
||||
+ path
|
||||
+ ", otherPaths="
|
||||
+ Arrays.toString(otherPaths)
|
||||
+ ", cameraType="
|
||||
+ cameraType
|
||||
+ ", deactivated="
|
||||
+ deactivated
|
||||
+ ", cameraQuirks="
|
||||
+ cameraQuirks
|
||||
+ ", FOV="
|
||||
@@ -227,4 +243,25 @@ public class CameraConfiguration {
|
||||
+ driveModeSettings
|
||||
+ "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* UICameraConfiguration has some stuff particular to VisionModule, but enough of it's common to
|
||||
* warrant this helper
|
||||
*/
|
||||
public UICameraConfiguration toUiConfig() {
|
||||
var ret = new UICameraConfiguration();
|
||||
|
||||
ret.matchedCameraInfo = matchedCameraInfo;
|
||||
ret.cameraPath = getDevicePath();
|
||||
ret.nickname = nickname;
|
||||
ret.uniqueName = uniqueName;
|
||||
ret.deactivated = deactivated;
|
||||
ret.isCSICamera = matchedCameraInfo.type() == CameraType.ZeroCopyPicam;
|
||||
ret.pipelineNicknames = pipelineSettings.stream().map(it -> it.pipelineNickname).toList();
|
||||
ret.cameraQuirks = cameraQuirks;
|
||||
ret.calibrations =
|
||||
calibrations.stream().map(CameraCalibrationCoefficients::cloneWithoutObservations).toList();
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +197,11 @@ public class ConfigManager {
|
||||
requestSave();
|
||||
}
|
||||
|
||||
public void addCameraConfiguration(CameraConfiguration config) {
|
||||
getConfig().addCameraConfig(config);
|
||||
requestSave();
|
||||
}
|
||||
|
||||
public void saveModule(CameraConfiguration config, String uniqueName) {
|
||||
getConfig().addCameraConfig(uniqueName, config);
|
||||
requestSave();
|
||||
|
||||
@@ -34,14 +34,6 @@ public class NetworkConfig {
|
||||
public boolean shouldManage;
|
||||
public boolean shouldPublishProto = false;
|
||||
|
||||
/**
|
||||
* If we should ONLY match cameras by path, and NEVER only by base-name. For now default to false
|
||||
* to preserve old matching logic.
|
||||
*
|
||||
* <p>This also disables creating new CameraConfigurations for detected "new" cameras.
|
||||
*/
|
||||
public boolean matchCamerasOnlyByPath = false;
|
||||
|
||||
@JsonIgnore public static final String NM_IFACE_STRING = "${interface}";
|
||||
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
|
||||
|
||||
@@ -70,8 +62,7 @@ public class NetworkConfig {
|
||||
@JsonProperty("shouldPublishProto") boolean shouldPublishProto,
|
||||
@JsonProperty("networkManagerIface") String networkManagerIface,
|
||||
@JsonProperty("setStaticCommand") String setStaticCommand,
|
||||
@JsonProperty("setDHCPcommand") String setDHCPcommand,
|
||||
@JsonProperty("matchCamerasOnlyByPath") boolean matchCamerasOnlyByPath) {
|
||||
@JsonProperty("setDHCPcommand") String setDHCPcommand) {
|
||||
this.ntServerAddress = ntServerAddress;
|
||||
this.connectionType = connectionType;
|
||||
this.staticIp = staticIp;
|
||||
@@ -81,7 +72,6 @@ public class NetworkConfig {
|
||||
this.networkManagerIface = networkManagerIface;
|
||||
this.setStaticCommand = setStaticCommand;
|
||||
this.setDHCPcommand = setDHCPcommand;
|
||||
this.matchCamerasOnlyByPath = matchCamerasOnlyByPath;
|
||||
setShouldManage(shouldManage);
|
||||
}
|
||||
|
||||
@@ -96,8 +86,7 @@ public class NetworkConfig {
|
||||
config.shouldPublishProto,
|
||||
config.networkManagerIface,
|
||||
config.setStaticCommand,
|
||||
config.setDHCPcommand,
|
||||
config.matchCamerasOnlyByPath);
|
||||
config.setDHCPcommand);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.photonvision.common.configuration;
|
||||
|
||||
import edu.wpi.first.apriltag.AprilTagFieldLayout;
|
||||
import edu.wpi.first.apriltag.AprilTagFields;
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
@@ -30,6 +31,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.configuration.CameraConfiguration.LegacyCameraConfigStruct;
|
||||
import org.photonvision.common.configuration.DatabaseSchema.Columns;
|
||||
import org.photonvision.common.configuration.DatabaseSchema.Tables;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
@@ -165,7 +167,8 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
if (userVersion < expectedVersion) {
|
||||
// older database, run migrations
|
||||
|
||||
// first, check to see if this is one of the ones from 2024 beta that need special handling
|
||||
// first, check to see if this is one of the ones from 2024 beta that need
|
||||
// special handling
|
||||
if (userVersion == 0 && getSchemaVersion() > 0) {
|
||||
String sql =
|
||||
"SELECT COUNT(*) AS CNTREC FROM pragma_table_info('cameras') WHERE name='otherpaths_json';";
|
||||
@@ -238,7 +241,8 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
try {
|
||||
conn.close();
|
||||
} catch (SQLException e) {
|
||||
// TODO, does the file still save if the SQL connection isn't closed correctly? If so,
|
||||
// TODO, does the file still save if the SQL connection isn't closed correctly?
|
||||
// If so,
|
||||
// return false here.
|
||||
logger.error("SQL Err closing connection while saving to disk: ", e);
|
||||
}
|
||||
@@ -365,12 +369,11 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
// Replace this camera's row with the new settings
|
||||
var sqlString =
|
||||
String.format(
|
||||
"REPLACE INTO %s (%s, %s, %s, %s, %s) VALUES (?,?,?,?,?);",
|
||||
"REPLACE INTO %s (%s, %s, %s, %s) VALUES (?,?,?,?);",
|
||||
Tables.CAMERAS,
|
||||
Columns.CAM_UNIQUE_NAME,
|
||||
Columns.CAM_CONFIG_JSON,
|
||||
Columns.CAM_DRIVERMODE_JSON,
|
||||
Columns.CAM_OTHERPATHS_JSON,
|
||||
Columns.CAM_PIPELINE_JSONS);
|
||||
|
||||
for (var c : config.getCameraConfigurations().entrySet()) {
|
||||
@@ -380,7 +383,6 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
statement.setString(1, c.getKey());
|
||||
statement.setString(2, JacksonUtils.serializeToString(config));
|
||||
statement.setString(3, JacksonUtils.serializeToString(config.driveModeSettings));
|
||||
statement.setString(4, JacksonUtils.serializeToString(config.otherPaths));
|
||||
|
||||
// Serializing a list of abstract classes sucks. Instead, make it into an array
|
||||
// of strings, which we can later unpack back into individual settings
|
||||
@@ -397,7 +399,7 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
statement.setString(5, JacksonUtils.serializeToString(settings));
|
||||
statement.setString(4, JacksonUtils.serializeToString(settings));
|
||||
|
||||
statement.executeUpdate();
|
||||
}
|
||||
@@ -433,7 +435,8 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
// In the future, this may not be needed. A better architecture would involve
|
||||
// manipulating the RAM representation of configuration when new .json files
|
||||
// are uploaded in the UI, and eliminate all other usages of saveOneFile().
|
||||
// But, seeing as it's Dec 28 and kickoff is nigh, we put this here and moved on.
|
||||
// But, seeing as it's Dec 28 and kickoff is nigh, we put this here and moved
|
||||
// on.
|
||||
// Thank you for coming to my TED talk.
|
||||
private boolean skipSavingHWCfg = false;
|
||||
private boolean skipSavingHWSet = false;
|
||||
@@ -586,14 +589,32 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
List<String> dummyList = new ArrayList<>();
|
||||
|
||||
var uniqueName = result.getString(Columns.CAM_UNIQUE_NAME);
|
||||
var config =
|
||||
JacksonUtils.deserialize(
|
||||
result.getString(Columns.CAM_CONFIG_JSON), CameraConfiguration.class);
|
||||
|
||||
// A horrifying hack to keep backward compat with otherpaths
|
||||
// We -really- need to delete this -stupid- otherpaths column. I hate it.
|
||||
var configStr = result.getString(Columns.CAM_CONFIG_JSON);
|
||||
CameraConfiguration config = JacksonUtils.deserialize(configStr, CameraConfiguration.class);
|
||||
|
||||
if (config.matchedCameraInfo == null) {
|
||||
logger.info("Legacy CameraConfiguration detected - upgrading");
|
||||
|
||||
// manually create the matchedCameraInfo ourselves. Need to upgrade:
|
||||
// baseName, path, otherPaths, cameraType, usbvid/pid -> matchedCameraInfo
|
||||
config.matchedCameraInfo =
|
||||
JacksonUtils.deserialize(configStr, LegacyCameraConfigStruct.class).matchedCameraInfo;
|
||||
|
||||
// Except that otherPaths used to be its own column. so hack that in here as well
|
||||
var otherPaths =
|
||||
JacksonUtils.deserialize(
|
||||
result.getString(Columns.CAM_OTHERPATHS_JSON), String[].class);
|
||||
if (config.matchedCameraInfo instanceof UsbCameraInfo usbInfo) {
|
||||
usbInfo.otherPaths = otherPaths;
|
||||
}
|
||||
}
|
||||
|
||||
var driverMode =
|
||||
JacksonUtils.deserialize(
|
||||
result.getString(Columns.CAM_DRIVERMODE_JSON), DriverModePipelineSettings.class);
|
||||
var otherPaths =
|
||||
JacksonUtils.deserialize(result.getString(Columns.CAM_OTHERPATHS_JSON), String[].class);
|
||||
List<?> pipelineSettings =
|
||||
JacksonUtils.deserialize(
|
||||
result.getString(Columns.CAM_PIPELINE_JSONS), dummyList.getClass());
|
||||
@@ -607,7 +628,6 @@ public class SqlConfigProvider extends ConfigProvider {
|
||||
|
||||
config.pipelineSettings = loadedSettings;
|
||||
config.driveModeSettings = driverMode;
|
||||
config.otherPaths = otherPaths;
|
||||
loadedConfigurations.put(uniqueName, config);
|
||||
}
|
||||
} catch (SQLException | IOException e) {
|
||||
|
||||
@@ -29,6 +29,25 @@ import org.photonvision.common.logging.Logger;
|
||||
public class DataChangeService {
|
||||
private static final Logger logger = new Logger(DataChangeService.class, LogGroup.WebServer);
|
||||
|
||||
public static class SubscriberHandle {
|
||||
private final int[] idxs;
|
||||
|
||||
private SubscriberHandle(int[] idxs) {
|
||||
this.idxs = idxs;
|
||||
}
|
||||
|
||||
private SubscriberHandle(int idx) {
|
||||
this.idxs = new int[] {idx};
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
for (int idx : idxs) {
|
||||
if (idx < 0) continue;
|
||||
getInstance().subscribers.set(idx, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ThreadSafeSingleton {
|
||||
private static final DataChangeService INSTANCE = new DataChangeService();
|
||||
}
|
||||
@@ -60,6 +79,7 @@ public class DataChangeService {
|
||||
try {
|
||||
var taken = eventQueue.take();
|
||||
for (var sub : subscribers) {
|
||||
if (sub == null) continue;
|
||||
if (sub.wantedSources.contains(taken.sourceType)
|
||||
&& sub.wantedDestinations.contains(taken.destType)) {
|
||||
sub.onDataChangeEvent(taken);
|
||||
@@ -72,9 +92,10 @@ public class DataChangeService {
|
||||
}
|
||||
}
|
||||
|
||||
public void addSubscriber(DataChangeSubscriber subscriber) {
|
||||
public SubscriberHandle addSubscriber(DataChangeSubscriber subscriber) {
|
||||
if (!subscribers.addIfAbsent(subscriber)) {
|
||||
logger.warn("Attempted to add already added subscriber!");
|
||||
return new SubscriberHandle(-1);
|
||||
} else {
|
||||
logger.debug(
|
||||
() -> {
|
||||
@@ -89,13 +110,16 @@ public class DataChangeService {
|
||||
|
||||
return "Added subscriber - " + "Sources: " + sources + ", Destinations: " + dests;
|
||||
});
|
||||
return new SubscriberHandle(subscribers.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void addSubscribers(DataChangeSubscriber... subs) {
|
||||
for (var sub : subs) {
|
||||
addSubscriber(sub);
|
||||
public SubscriberHandle addSubscribers(DataChangeSubscriber... subs) {
|
||||
int[] idxs = new int[subs.length];
|
||||
for (int i = 0; i < subs.length; i++) {
|
||||
idxs[i] = addSubscriber(subs[i]).idxs[0];
|
||||
}
|
||||
return new SubscriberHandle(idxs);
|
||||
}
|
||||
|
||||
public void publishEvent(DataChangeEvent event) {
|
||||
|
||||
@@ -19,15 +19,24 @@ package org.photonvision.common.dataflow.websocket;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.vision.calibration.UICameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
|
||||
public class UICameraConfiguration {
|
||||
@SuppressWarnings("unused")
|
||||
public double fov;
|
||||
// Path to the camera device. On Linux, this is a special file in /dev/v4l/by-id
|
||||
// or /dev/videoN.
|
||||
// This is the path we hand to CSCore to do auto-reconnect on
|
||||
public String cameraPath;
|
||||
|
||||
/** See {@link CameraConfiguration #deactivated} */
|
||||
public boolean deactivated;
|
||||
|
||||
public String nickname;
|
||||
public String uniqueName;
|
||||
|
||||
public double fov;
|
||||
public HashMap<String, Object> currentPipelineSettings;
|
||||
public int currentPipelineIndex;
|
||||
public List<String> pipelineNicknames;
|
||||
@@ -42,4 +51,9 @@ public class UICameraConfiguration {
|
||||
public double maxExposureRaw;
|
||||
public double minWhiteBalanceTemp;
|
||||
public double maxWhiteBalanceTemp;
|
||||
public PVCameraInfo matchedCameraInfo;
|
||||
|
||||
// Status for if the underlying device is present and such
|
||||
public boolean isConnected;
|
||||
public boolean hasConnected;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public class UIDataPublisher implements CVPipelineResultConsumer {
|
||||
if (lastUIResultUpdateTime + 1000.0 / 10.0 > now) return;
|
||||
|
||||
var dataMap = new HashMap<String, Object>();
|
||||
dataMap.put("sequenceID", result.sequenceID);
|
||||
dataMap.put("fps", result.fps);
|
||||
dataMap.put("latency", result.getLatencyMillis());
|
||||
var uiTargets = new ArrayList<HashMap<String, Object>>(result.targets.size());
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.photonvision.common.networking.NetworkUtils;
|
||||
import org.photonvision.mrcal.MrCalJNILoader;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
import org.photonvision.vision.processes.VisionSourceManager;
|
||||
|
||||
public class UIPhotonConfiguration {
|
||||
public List<UICameraConfiguration> cameraSettings;
|
||||
@@ -62,7 +62,7 @@ public class UIPhotonConfiguration {
|
||||
: c.getHardwareConfig().deviceName,
|
||||
Platform.getPlatformName()),
|
||||
c.getApriltagFieldLayout()),
|
||||
VisionModuleManager.getInstance().getModules().stream()
|
||||
VisionSourceManager.getInstance().getVisionModules().stream()
|
||||
.map(VisionModule::toUICameraConfig)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
|
||||
public class CameraInfo extends UsbCameraInfo {
|
||||
public final CameraType cameraType;
|
||||
|
||||
public CameraInfo(
|
||||
int dev, String path, String name, String[] otherPaths, int vendorId, int productId) {
|
||||
super(dev, path, name, otherPaths, vendorId, productId);
|
||||
cameraType = CameraType.UsbCamera;
|
||||
}
|
||||
|
||||
public CameraInfo(
|
||||
int dev,
|
||||
String path,
|
||||
String name,
|
||||
String[] otherPaths,
|
||||
int vendorId,
|
||||
int productId,
|
||||
CameraType cameraType) {
|
||||
super(dev, path, name, otherPaths, vendorId, productId);
|
||||
this.cameraType = cameraType;
|
||||
}
|
||||
|
||||
public CameraInfo(UsbCameraInfo info) {
|
||||
super(info.dev, info.path, info.name, info.otherPaths, info.vendorId, info.productId);
|
||||
cameraType = CameraType.UsbCamera;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return True, if this camera is reported from V4L and is a CSI camera.
|
||||
*/
|
||||
public boolean getIsV4lCsiCamera() {
|
||||
return (Arrays.stream(otherPaths).anyMatch(it -> it.contains("csi-video"))
|
||||
|| getBaseName().equals("unicam"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The base name of the camera aka the name as just ascii.
|
||||
*/
|
||||
public String getBaseName() {
|
||||
return name.replaceAll("[^\\x00-\\x7F]", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Returns a human readable name
|
||||
*/
|
||||
public String getHumanReadableName() {
|
||||
return getBaseName().replaceAll(" ", "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a unique descriptor of the USB port this camera is attached to. EG
|
||||
* "/dev/v4l/by-path/platform-fc800000.usb-usb-0:1.3:1.0-video-index0"
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Optional<String> getUSBPath() {
|
||||
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (getClass() != obj.getClass()) return false;
|
||||
CameraInfo other = (CameraInfo) obj;
|
||||
|
||||
// Windows device number is not significant. See
|
||||
// https://github.com/wpilibsuite/allwpilib/blob/4b94a64b06057c723d6fcafeb1a45f55a70d179a/cscore/src/main/native/windows/UsbCameraImpl.cpp#L1128
|
||||
if (!Platform.isWindows()) {
|
||||
if (dev != other.dev) return false;
|
||||
}
|
||||
|
||||
if (!path.equals(other.path)) return false;
|
||||
if (!name.equals(other.name)) return false;
|
||||
if (!Arrays.asList(this.getUSBPath()).contains(other.getUSBPath())) return false;
|
||||
if (vendorId != other.vendorId) return false;
|
||||
if (productId != other.productId) return false;
|
||||
|
||||
// Don't trust super.equals, as it compares references. Should PR this to allwpilib at some
|
||||
// point
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CameraInfo [cameraType="
|
||||
+ cameraType
|
||||
+ ", baseName="
|
||||
+ getBaseName()
|
||||
+ ", vid="
|
||||
+ vendorId
|
||||
+ ", pid="
|
||||
+ productId
|
||||
+ ", path="
|
||||
+ path
|
||||
+ ", otherPaths="
|
||||
+ Arrays.toString(otherPaths)
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,6 @@ package org.photonvision.vision.camera;
|
||||
|
||||
public enum CameraType {
|
||||
UsbCamera,
|
||||
HttpCamera,
|
||||
ZeroCopyPicam
|
||||
ZeroCopyPicam,
|
||||
FileCamera // special case for File-based vision sources
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import edu.wpi.first.util.PixelFormat;
|
||||
import java.nio.file.Path;
|
||||
@@ -40,7 +41,8 @@ public class FileVisionSource extends VisionSource {
|
||||
: null;
|
||||
frameProvider =
|
||||
new FileFrameProvider(
|
||||
Path.of(cameraConfiguration.path),
|
||||
// TODO - create new File/replay camera info type
|
||||
Path.of(cameraConfiguration.getDevicePath()),
|
||||
cameraConfiguration.FOV,
|
||||
FileFrameProvider.MAX_FPS,
|
||||
calibration);
|
||||
@@ -53,7 +55,12 @@ public class FileVisionSource extends VisionSource {
|
||||
}
|
||||
|
||||
public FileVisionSource(String name, String imagePath, double fov) {
|
||||
super(new CameraConfiguration(name, imagePath));
|
||||
// TODO - create new File/replay camera info type
|
||||
super(
|
||||
new CameraConfiguration(
|
||||
PVCameraInfo.fromUsbCameraInfo(new UsbCameraInfo(0, imagePath, name, null, 0, 0)),
|
||||
name,
|
||||
name));
|
||||
frameProvider = new FileFrameProvider(imagePath, fov);
|
||||
settables =
|
||||
new FileSourceSettables(cameraConfiguration, frameProvider.get().frameStaticProperties);
|
||||
@@ -85,7 +92,12 @@ public class FileVisionSource extends VisionSource {
|
||||
return false; // Assume USB cameras do not have photonvision-controlled LEDs
|
||||
}
|
||||
|
||||
private static class FileSourceSettables extends VisionSourceSettables {
|
||||
@Override
|
||||
public void release() {
|
||||
frameProvider.release();
|
||||
}
|
||||
|
||||
public static class FileSourceSettables extends VisionSourceSettables {
|
||||
private final VideoMode videoMode;
|
||||
|
||||
private final HashMap<Integer, VideoMode> videoModes = new HashMap<>();
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.annotation.JsonSubTypes;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import java.util.Arrays;
|
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT)
|
||||
@JsonSubTypes({
|
||||
@JsonSubTypes.Type(value = PVCameraInfo.PVUsbCameraInfo.class),
|
||||
@JsonSubTypes.Type(value = PVCameraInfo.PVCSICameraInfo.class),
|
||||
@JsonSubTypes.Type(value = PVCameraInfo.PVFileCameraInfo.class)
|
||||
})
|
||||
public sealed interface PVCameraInfo {
|
||||
/**
|
||||
* @return The path of the camera.
|
||||
*/
|
||||
String path();
|
||||
|
||||
/**
|
||||
* @return The base name of the camera aka the name as just ascii.
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* @return Returns a human readable name
|
||||
*/
|
||||
default String humanReadableName() {
|
||||
return name().replaceAll(" ", "_");
|
||||
}
|
||||
|
||||
/**
|
||||
* If the camera is a USB camera this method returns a unique descriptor of the USB port this
|
||||
* camera is attached to. EG "/dev/v4l/by-path/platform-fc800000.usb-usb-0:1.3:1.0-video-index0".
|
||||
* If the camera is a CSI camera this method returns the path of the camera.
|
||||
*
|
||||
* <p>If we are on Windows, this will return the opaque path as described by
|
||||
* MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_SYMBOLIC_LINK (see
|
||||
* https://learn.microsoft.com/en-us/windows/win32/medfound/mf-devsource-attribute-source-type-vidcap-symbolic-link)
|
||||
*
|
||||
* @return The unique path of the camera
|
||||
*/
|
||||
@JsonGetter(value = "uniquePath")
|
||||
String uniquePath();
|
||||
|
||||
String[] otherPaths();
|
||||
|
||||
CameraType type();
|
||||
|
||||
default boolean equals(PVCameraInfo other) {
|
||||
return uniquePath().equals(other.uniquePath());
|
||||
}
|
||||
|
||||
@JsonTypeName("PVUsbCameraInfo")
|
||||
public static final class PVUsbCameraInfo extends UsbCameraInfo implements PVCameraInfo {
|
||||
@JsonCreator
|
||||
public PVUsbCameraInfo(
|
||||
@JsonProperty("dev") int dev,
|
||||
@JsonProperty("path") String path,
|
||||
@JsonProperty("name") String name,
|
||||
@JsonProperty("otherPaths") String[] otherPaths,
|
||||
@JsonProperty("vendorId") int vendorId,
|
||||
@JsonProperty("productId") int productId) {
|
||||
super(dev, path, name, otherPaths, vendorId, productId);
|
||||
}
|
||||
|
||||
private PVUsbCameraInfo(UsbCameraInfo info) {
|
||||
super(info.dev, info.path, info.name, info.otherPaths, info.vendorId, info.productId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String path() {
|
||||
return super.path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return super.name.replaceAll("[^\\x00-\\x7F]", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uniquePath() {
|
||||
return Arrays.stream(super.otherPaths)
|
||||
.filter(path -> path.contains("/by-path/"))
|
||||
.findFirst()
|
||||
.orElse(path());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] otherPaths() {
|
||||
return super.otherPaths;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CameraType type() {
|
||||
return CameraType.UsbCamera;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (obj instanceof PVCameraInfo info) {
|
||||
return equals(info);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PVUsbCameraInfo[type="
|
||||
+ type()
|
||||
+ ", dev="
|
||||
+ super.dev
|
||||
+ ", path='"
|
||||
+ super.path
|
||||
+ "', name='"
|
||||
+ super.name
|
||||
+ "', otherPaths="
|
||||
+ Arrays.toString(super.otherPaths)
|
||||
+ ", vid="
|
||||
+ super.vendorId
|
||||
+ ", pid="
|
||||
+ super.productId
|
||||
+ ", uniquePath='"
|
||||
+ uniquePath()
|
||||
+ "']";
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("PVCSICameraInfo")
|
||||
public static final class PVCSICameraInfo implements PVCameraInfo {
|
||||
public final String path;
|
||||
public final String baseName;
|
||||
|
||||
@JsonCreator
|
||||
public PVCSICameraInfo(
|
||||
@JsonProperty("path") String path, @JsonProperty("baseName") String baseName) {
|
||||
this.path = path;
|
||||
this.baseName = baseName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return baseName.replaceAll("[^\\x00-\\x7F]", "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uniquePath() {
|
||||
return path();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] otherPaths() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public CameraType type() {
|
||||
return CameraType.ZeroCopyPicam;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (obj instanceof PVCameraInfo info) {
|
||||
return equals(info);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PVCsiCameraInfo[type="
|
||||
+ type()
|
||||
+ ", basename="
|
||||
+ baseName
|
||||
+ ", path='"
|
||||
+ path
|
||||
+ "', uniquePath='"
|
||||
+ uniquePath()
|
||||
+ "']";
|
||||
}
|
||||
}
|
||||
|
||||
@JsonTypeName("PVFileCameraInfo")
|
||||
public static final class PVFileCameraInfo implements PVCameraInfo {
|
||||
public final String path;
|
||||
public final String name;
|
||||
|
||||
@JsonCreator
|
||||
public PVFileCameraInfo(@JsonProperty("path") String path, @JsonProperty("name") String name) {
|
||||
this.path = path;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uniquePath() {
|
||||
return path();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] otherPaths() {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public CameraType type() {
|
||||
return CameraType.FileCamera;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null) return false;
|
||||
if (obj instanceof PVFileCameraInfo info) {
|
||||
return equals(info);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PVFileCameraInfo[type=" + type() + ", filename=" + name + ", path='" + path + "']";
|
||||
}
|
||||
}
|
||||
|
||||
public static PVCameraInfo fromUsbCameraInfo(UsbCameraInfo info) {
|
||||
return new PVUsbCameraInfo(info);
|
||||
}
|
||||
|
||||
public static PVCameraInfo fromCSICameraInfo(String path, String baseName) {
|
||||
return new PVCSICameraInfo(path, baseName);
|
||||
}
|
||||
|
||||
public static PVCameraInfo fromFileInfo(String path, String baseName) {
|
||||
return new PVFileCameraInfo(path, baseName);
|
||||
}
|
||||
}
|
||||
@@ -29,18 +29,17 @@ import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
/** Dummy class for unit testing the vision source manager */
|
||||
public class TestSource extends VisionSource {
|
||||
private FrameProvider usbFrameProvider;
|
||||
|
||||
public TestSource(CameraConfiguration config) {
|
||||
super(config);
|
||||
|
||||
getCameraConfiguration().cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(config.usbVID, config.usbVID, config.baseName);
|
||||
// Disable camera quirk detection using this fun hack
|
||||
getCameraConfiguration().cameraQuirks = QuirkyCamera.getQuirkyCamera(-1, -1, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remakeSettables() {
|
||||
// Nothing to do, settables for this type of VisionSource should never be remade.
|
||||
// Nothing to do, settables for this type of VisionSource should never be
|
||||
// remade.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -81,6 +80,22 @@ public class TestSource extends VisionSource {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'requestHsvSettings'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'release'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkCameraConnected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -100,4 +115,10 @@ public class TestSource extends VisionSource {
|
||||
public boolean hasLEDs() {
|
||||
return false; // Assume USB cameras do not have photonvision-controlled LEDs
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
// TODO Auto-generated method stub
|
||||
throw new UnsupportedOperationException("Unimplemented method 'close'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,8 +67,7 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
this.configuration = configuration;
|
||||
this.camera = camera;
|
||||
|
||||
getAllVideoModes();
|
||||
|
||||
// TODO - how should this work post-refactor???
|
||||
if (!configuration.cameraQuirks.hasQuirk(CameraQuirk.StickyFPS)) {
|
||||
if (!videoModes.isEmpty()) {
|
||||
setVideoMode(videoModes.get(0)); // fixes double FPS set
|
||||
@@ -91,6 +90,15 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
findProperty(
|
||||
"raw_exposure_absolute", "raw_exposure_time_absolute", "exposure", "raw_Exposure");
|
||||
|
||||
if (expProp.isEmpty()) {
|
||||
logger.warn("Could not find exposure property");
|
||||
return;
|
||||
} else {
|
||||
exposureAbsProp = expProp.get();
|
||||
this.minExposure = exposureAbsProp.getMin();
|
||||
this.maxExposure = exposureAbsProp.getMax();
|
||||
}
|
||||
|
||||
// Photonvision needs to be able to control auto exposure. Make sure we can
|
||||
// first.
|
||||
var autoExpProp = findProperty("exposure_auto", "auto_exposure");
|
||||
@@ -262,55 +270,68 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Integer, VideoMode> getAllVideoModes() {
|
||||
if (videoModes == null) {
|
||||
videoModes = new HashMap<>();
|
||||
List<VideoMode> videoModesList = new ArrayList<>();
|
||||
try {
|
||||
VideoMode[] modes;
|
||||
private void cacheVideoModes() {
|
||||
videoModes = new HashMap<>();
|
||||
List<VideoMode> videoModesList = new ArrayList<>();
|
||||
try {
|
||||
VideoMode[] modes;
|
||||
|
||||
modes = camera.enumerateVideoModes();
|
||||
modes = camera.enumerateVideoModes();
|
||||
|
||||
for (VideoMode videoMode : modes) {
|
||||
// Filter grey modes
|
||||
if (videoMode.pixelFormat == PixelFormat.kGray
|
||||
|| videoMode.pixelFormat == PixelFormat.kUnknown) {
|
||||
for (VideoMode videoMode : modes) {
|
||||
// Filter grey modes
|
||||
if (videoMode.pixelFormat == PixelFormat.kGray
|
||||
|| videoMode.pixelFormat == PixelFormat.kUnknown) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (configuration.cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) {
|
||||
if (videoMode.fps > 100) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (configuration.cameraQuirks.hasQuirk(CameraQuirk.FPSCap100)) {
|
||||
if (videoMode.fps > 100) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
videoModesList.add(videoMode);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception while enumerating video modes!", e);
|
||||
videoModesList = List.of();
|
||||
}
|
||||
|
||||
// Sort by resolution
|
||||
var sortedList =
|
||||
videoModesList.stream()
|
||||
.distinct() // remove redundant video mode entries
|
||||
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
|
||||
.collect(Collectors.toList());
|
||||
Collections.reverse(sortedList);
|
||||
|
||||
// On vendor cameras, respect blacklisted indices
|
||||
var indexBlacklist =
|
||||
ConfigManager.getInstance().getConfig().getHardwareConfig().blacklistedResIndices;
|
||||
for (int badIdx : indexBlacklist) {
|
||||
sortedList.remove(badIdx);
|
||||
}
|
||||
|
||||
for (VideoMode videoMode : sortedList) {
|
||||
videoModes.put(sortedList.indexOf(videoMode), videoMode);
|
||||
videoModesList.add(videoMode);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception while enumerating video modes!", e);
|
||||
videoModesList = List.of();
|
||||
}
|
||||
|
||||
// Sort by resolution
|
||||
var sortedList =
|
||||
videoModesList.stream()
|
||||
.distinct() // remove redundant video mode entries
|
||||
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
|
||||
.collect(Collectors.toList());
|
||||
Collections.reverse(sortedList);
|
||||
|
||||
// On vendor cameras, respect blacklisted indices
|
||||
var indexBlacklist =
|
||||
ConfigManager.getInstance().getConfig().getHardwareConfig().blacklistedResIndices;
|
||||
for (int badIdx : indexBlacklist) {
|
||||
sortedList.remove(badIdx);
|
||||
}
|
||||
|
||||
for (VideoMode videoMode : sortedList) {
|
||||
videoModes.put(sortedList.indexOf(videoMode), videoMode);
|
||||
}
|
||||
|
||||
// If after all that we still have no video modes, not much we can do besides
|
||||
// throw up our hands
|
||||
if (videoModes.isEmpty()) {
|
||||
logger.info("Camera " + camera.getPath() + " has no video modes supported by PhotonVision");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public HashMap<Integer, VideoMode> getAllVideoModes() {
|
||||
if (!cameraPropertiesCached) {
|
||||
// Device hasn't connected at least once, best I can do is given up
|
||||
logger.warn("Device hasn't connected, cannot enumerate video modes");
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
return videoModes;
|
||||
}
|
||||
|
||||
@@ -372,4 +393,21 @@ public class GenericUSBCameraSettables extends VisionSourceSettables {
|
||||
public double getMinWhiteBalanceTemp() {
|
||||
return minWhiteBalanceTemp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraConnected() {
|
||||
super.onCameraConnected();
|
||||
|
||||
logger.info("Caching cscore properties");
|
||||
|
||||
// Now that our device is actually connected, we can enumerate properties/video
|
||||
// modes
|
||||
setUpExposureProperties();
|
||||
setUpWhiteBalanceProperties();
|
||||
cacheVideoModes();
|
||||
|
||||
setAllCamDefaults();
|
||||
|
||||
calculateFrameStaticProps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
package org.photonvision.vision.camera.USBCameras;
|
||||
|
||||
import edu.wpi.first.cameraserver.CameraServer;
|
||||
import edu.wpi.first.cscore.CvSink;
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import edu.wpi.first.cscore.VideoException;
|
||||
import edu.wpi.first.cscore.VideoProperty;
|
||||
@@ -28,6 +27,7 @@ import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.camera.PVCameraInfo.PVUsbCameraInfo;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.provider.USBFrameProvider;
|
||||
@@ -39,37 +39,54 @@ public class USBCameraSource extends VisionSource {
|
||||
private final UsbCamera camera;
|
||||
protected GenericUSBCameraSettables settables;
|
||||
protected FrameProvider usbFrameProvider;
|
||||
private final CvSink cvSink;
|
||||
|
||||
private void onCameraConnected() {
|
||||
// Aid to the development team - record the properties available for whatever the user plugged
|
||||
// in
|
||||
printCameraProperaties();
|
||||
|
||||
settables.onCameraConnected();
|
||||
}
|
||||
|
||||
public USBCameraSource(CameraConfiguration config) {
|
||||
super(config);
|
||||
|
||||
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
|
||||
// cscore will auto-reconnect to the camera path we give it. v4l does not guarantee that if i
|
||||
// swap cameras around, the same /dev/videoN ID will be assigned to that camera. So instead
|
||||
// default to pinning to a particular USB port, or by "path" (appears to be a global identifier)
|
||||
// on Windows.
|
||||
camera = new UsbCamera(config.nickname, config.getUSBPath().orElse(config.path));
|
||||
cvSink = CameraServer.getVideo(this.camera);
|
||||
|
||||
// set vid/pid if not done already for future matching
|
||||
if (config.usbVID <= 0) config.usbVID = this.camera.getInfo().vendorId;
|
||||
if (config.usbPID <= 0) config.usbPID = this.camera.getInfo().productId;
|
||||
if (!(config.matchedCameraInfo instanceof PVUsbCameraInfo)) {
|
||||
logger.error(
|
||||
"USBCameraSource matched to a non-USB camera info?? "
|
||||
+ config.matchedCameraInfo.toString());
|
||||
}
|
||||
|
||||
camera = new UsbCamera(config.nickname, config.getDevicePath());
|
||||
|
||||
// TODO - I don't need this, do I?
|
||||
// // set vid/pid if not done already for future matching
|
||||
// if (config.usbVID <= 0) config.usbVID = this.camera.getInfo().vendorId;
|
||||
// if (config.usbPID <= 0) config.usbPID = this.camera.getInfo().productId;
|
||||
|
||||
// TODO - why do we delegate this to USBCameraSource? Quirks are part of the CameraConfig??
|
||||
// also TODO - is the config's saved usb info a reasonable guess for quirk detection? seems like
|
||||
// yes to me...
|
||||
if (getCameraConfiguration().cameraQuirks == null) {
|
||||
int vid =
|
||||
(config.matchedCameraInfo instanceof PVUsbCameraInfo)
|
||||
? ((PVUsbCameraInfo) config.matchedCameraInfo).vendorId
|
||||
: -1;
|
||||
int pid =
|
||||
(config.matchedCameraInfo instanceof PVUsbCameraInfo)
|
||||
? ((PVUsbCameraInfo) config.matchedCameraInfo).productId
|
||||
: -1;
|
||||
|
||||
getCameraConfiguration().cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(
|
||||
camera.getInfo().vendorId, camera.getInfo().productId, config.baseName);
|
||||
QuirkyCamera.getQuirkyCamera(vid, pid, config.matchedCameraInfo.name());
|
||||
}
|
||||
|
||||
if (getCameraConfiguration().cameraQuirks.hasQuirks()) {
|
||||
logger.info("Quirky camera detected: " + getCameraConfiguration().cameraQuirks.baseName);
|
||||
logger.info("Quirky camera detected: " + getCameraConfiguration().cameraQuirks);
|
||||
}
|
||||
|
||||
// Aid to the development team - record the properties available for whatever the user plugged
|
||||
// in
|
||||
printCameraProperaties();
|
||||
|
||||
var cameraBroken = getCameraConfiguration().cameraQuirks.hasQuirk(CameraQuirk.CompletelyBroken);
|
||||
|
||||
if (cameraBroken) {
|
||||
@@ -87,16 +104,7 @@ public class USBCameraSource extends VisionSource {
|
||||
settables = createSettables(config, camera);
|
||||
logger.info("Created settables " + settables);
|
||||
|
||||
if (settables.getAllVideoModes().isEmpty()) {
|
||||
// No video modes produced from settables, disable the camera
|
||||
logger.info("Camera " + camera.getPath() + " has no video modes supported by PhotonVision");
|
||||
usbFrameProvider = null;
|
||||
|
||||
} else {
|
||||
// Functional camera, set up the frame provider and configure defaults
|
||||
usbFrameProvider = new USBFrameProvider(cvSink, settables);
|
||||
settables.setAllCamDefaults();
|
||||
}
|
||||
usbFrameProvider = new USBFrameProvider(camera, settables, this::onCameraConnected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,9 +155,6 @@ public class USBCameraSource extends VisionSource {
|
||||
settables = new GenericUSBCameraSettables(config, camera);
|
||||
}
|
||||
|
||||
settables.setUpExposureProperties();
|
||||
settables.setUpWhiteBalanceProperties();
|
||||
|
||||
return settables;
|
||||
}
|
||||
|
||||
@@ -165,7 +170,14 @@ public class USBCameraSource extends VisionSource {
|
||||
var oldVideoMode = this.settables.getCurrentVideoMode();
|
||||
this.settables = createSettables(oldConfig, oldCamera);
|
||||
|
||||
// And update FrameStaticProps
|
||||
// Settables only cache videomodes on connect - force this to happen next tick
|
||||
if (settables.camera.isConnected()) {
|
||||
this.settables.onCameraConnected();
|
||||
} else {
|
||||
this.usbFrameProvider.cameraPropertiesCached = false;
|
||||
}
|
||||
|
||||
// And update the settables' FrameStaticProps
|
||||
settables.setVideoMode(oldVideoMode);
|
||||
|
||||
// Propogate our updated settables over to the frame provider
|
||||
@@ -228,6 +240,14 @@ public class USBCameraSource extends VisionSource {
|
||||
return false; // Assume USB cameras do not have photonvision-controlled LEDs
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
CameraServer.removeCamera(camera.getName());
|
||||
camera.close();
|
||||
usbFrameProvider.release();
|
||||
usbFrameProvider = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
@@ -243,9 +263,6 @@ public class USBCameraSource extends VisionSource {
|
||||
if (usbFrameProvider == null) {
|
||||
if (other.usbFrameProvider != null) return false;
|
||||
} else if (!usbFrameProvider.equals(other.usbFrameProvider)) return false;
|
||||
if (cvSink == null) {
|
||||
if (other.cvSink != null) return false;
|
||||
} else if (!cvSink.equals(other.cvSink)) return false;
|
||||
if (getCameraConfiguration().cameraQuirks == null) {
|
||||
if (other.getCameraConfiguration().cameraQuirks != null) return false;
|
||||
} else if (!getCameraConfiguration()
|
||||
@@ -261,7 +278,6 @@ public class USBCameraSource extends VisionSource {
|
||||
settables,
|
||||
usbFrameProvider,
|
||||
cameraConfiguration,
|
||||
cvSink,
|
||||
getCameraConfiguration().cameraQuirks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
package org.photonvision.vision.camera.csi;
|
||||
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import edu.wpi.first.math.MathUtil;
|
||||
@@ -25,7 +25,7 @@ import java.util.HashMap;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.raspi.LibCameraJNI;
|
||||
import org.photonvision.vision.camera.LibcameraGpuSource.FPSRatedVideoMode;
|
||||
import org.photonvision.vision.camera.csi.LibcameraGpuSource.FPSRatedVideoMode;
|
||||
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
|
||||
@@ -64,7 +64,7 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
|
||||
videoModes = new HashMap<>();
|
||||
|
||||
sensorModel = LibCameraJNI.getSensorModel(configuration.path);
|
||||
sensorModel = LibCameraJNI.getSensorModel(configuration.matchedCameraInfo.path());
|
||||
|
||||
if (sensorModel == LibCameraJNI.SensorModel.IMX219) {
|
||||
// Settings for the IMX219 sensor, which is used on the Pi Camera Module v2
|
||||
@@ -113,6 +113,8 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
} else if (sensorModel == LibCameraJNI.SensorModel.OV5647) {
|
||||
minExposure = 560;
|
||||
}
|
||||
this.cameraPropertiesCached =
|
||||
true; // Camera properties are not able to be changed so they are always cached
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -213,7 +215,7 @@ public class LibcameraGpuSettables extends VisionSourceSettables {
|
||||
logger.debug("Creating libcamera");
|
||||
r_ptr =
|
||||
LibCameraJNI.createCamera(
|
||||
getConfiguration().path,
|
||||
getConfiguration().matchedCameraInfo.path(),
|
||||
mode.width,
|
||||
mode.height,
|
||||
(m_rotationMode == ImageRotationMode.DEG_180_CCW ? 180 : 0));
|
||||
@@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.camera;
|
||||
package org.photonvision.vision.camera.csi;
|
||||
|
||||
import edu.wpi.first.cscore.VideoMode;
|
||||
import edu.wpi.first.util.PixelFormat;
|
||||
@@ -23,6 +23,8 @@ import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.camera.CameraType;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.provider.LibcameraGpuFrameProvider;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
@@ -32,11 +34,11 @@ public class LibcameraGpuSource extends VisionSource {
|
||||
static final Logger logger = new Logger(LibcameraGpuSource.class, LogGroup.Camera);
|
||||
|
||||
private final LibcameraGpuSettables settables;
|
||||
private final LibcameraGpuFrameProvider frameProvider;
|
||||
private LibcameraGpuFrameProvider frameProvider;
|
||||
|
||||
public LibcameraGpuSource(CameraConfiguration configuration) {
|
||||
super(configuration);
|
||||
if (configuration.cameraType != CameraType.ZeroCopyPicam) {
|
||||
if (configuration.matchedCameraInfo.type() != CameraType.ZeroCopyPicam) {
|
||||
throw new IllegalArgumentException(
|
||||
"GPUAcceleratedPicamSource only accepts CameraConfigurations with type Picam");
|
||||
}
|
||||
@@ -60,7 +62,8 @@ public class LibcameraGpuSource extends VisionSource {
|
||||
|
||||
@Override
|
||||
public void remakeSettables() {
|
||||
// Nothing to do, settables for this type of VisionSource should never be remade.
|
||||
// Nothing to do, settables for this type of VisionSource should never be
|
||||
// remade.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -99,4 +102,10 @@ public class LibcameraGpuSource extends VisionSource {
|
||||
public boolean hasLEDs() {
|
||||
return (ConfigManager.getInstance().getConfig().getHardwareConfig().ledPins.size() > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
frameProvider.release();
|
||||
frameProvider = null;
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,33 @@ package org.photonvision.vision.frame;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import org.photonvision.vision.opencv.ImageRotationMode;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
import org.photonvision.vision.pipe.impl.HSVPipe;
|
||||
|
||||
public abstract class FrameProvider implements Supplier<Frame> {
|
||||
public abstract class FrameProvider implements Supplier<Frame>, Releasable {
|
||||
protected int sequenceID = 0;
|
||||
|
||||
// Escape hatch to allow us to synchronously (from the main vision thread) run
|
||||
// extra
|
||||
// setup/callbacks once cscore connects to our underlying device for the first
|
||||
// time
|
||||
public boolean cameraPropertiesCached = false;
|
||||
|
||||
protected void onCameraConnected() {
|
||||
cameraPropertiesCached = true;
|
||||
}
|
||||
|
||||
public abstract boolean isConnected();
|
||||
|
||||
public abstract boolean checkCameraConnected();
|
||||
|
||||
/**
|
||||
* Returns if the camera has connected at some point. This is not if it is currently connected.
|
||||
*/
|
||||
public boolean hasConnected() {
|
||||
return cameraPropertiesCached;
|
||||
}
|
||||
|
||||
public abstract String getName();
|
||||
|
||||
/** Ask the camera to produce a certain kind of processed image (e.g. HSV or greyscale) */
|
||||
|
||||
@@ -169,4 +169,8 @@ public class FileSaveFrameConsumer implements Consumer<CVMat> {
|
||||
matchTypes[MathUtil.clamp((int) matchType.value, 0, matchTypes.length - 1)];
|
||||
return matchTypeStr + "-" + matchNum.value + "-" + eventName.value;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
// troododfa;lkjadsf;lkfdsaj otgooadflsk;j
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,9 @@ public abstract class CpuImageProcessor extends FrameProvider {
|
||||
outputMat,
|
||||
m_processType,
|
||||
input.captureTimestamp,
|
||||
input.staticProps.rotate(m_rImagePipe.getParams().rotation));
|
||||
input.staticProps != null
|
||||
? input.staticProps.rotate(m_rImagePipe.getParams().rotation)
|
||||
: input.staticProps);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -27,12 +27,13 @@ import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
|
||||
/**
|
||||
* A {@link FrameProvider} that will read and provide an image from a {@link java.nio.file.Path
|
||||
* path}.
|
||||
*/
|
||||
public class FileFrameProvider extends CpuImageProcessor {
|
||||
public class FileFrameProvider extends CpuImageProcessor implements Releasable {
|
||||
public static final int MAX_FPS = 10;
|
||||
private static int count = 0;
|
||||
|
||||
@@ -106,7 +107,9 @@ public class FileFrameProvider extends CpuImageProcessor {
|
||||
try {
|
||||
Thread.sleep(millisDelay);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("FileFrameProvider interrupted - not busywaiting");
|
||||
// throw back up the stack
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,4 +121,24 @@ public class FileFrameProvider extends CpuImageProcessor {
|
||||
public String getName() {
|
||||
return "FileFrameProvider" + thisIndex + " - " + path.getFileName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
originalFrame.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkCameraConnected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasConnected() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.math.MathUtils;
|
||||
import org.photonvision.raspi.LibCameraJNI;
|
||||
import org.photonvision.vision.camera.LibcameraGpuSettables;
|
||||
import org.photonvision.vision.camera.csi.LibcameraGpuSettables;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.frame.FrameThresholdType;
|
||||
@@ -40,6 +40,8 @@ public class LibcameraGpuFrameProvider extends FrameProvider {
|
||||
|
||||
var vidMode = settables.getCurrentVideoMode();
|
||||
settables.setVideoMode(vidMode);
|
||||
this.cameraPropertiesCached =
|
||||
true; // Camera properties are not able to be changed so they are always cached
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -132,4 +134,30 @@ public class LibcameraGpuFrameProvider extends FrameProvider {
|
||||
public void requestFrameCopies(boolean copyInput, boolean copyOutput) {
|
||||
LibCameraJNI.setFramesToCopy(settables.r_ptr, copyInput, copyOutput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
synchronized (settables.CAMERA_LOCK) {
|
||||
LibCameraJNI.stopCamera(settables.r_ptr);
|
||||
LibCameraJNI.destroyCamera(settables.r_ptr);
|
||||
settables.r_ptr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkCameraConnected() {
|
||||
String[] cameraNames = LibCameraJNI.getCameraNames();
|
||||
for (String name : cameraNames) {
|
||||
if (name.equals(settables.getConfiguration().getDevicePath())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// To our knowledge the camera is always connected (after boot) with csi cameras
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return checkCameraConnected();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
|
||||
package org.photonvision.vision.frame.provider;
|
||||
|
||||
import edu.wpi.first.cameraserver.CameraServer;
|
||||
import edu.wpi.first.cscore.CvSink;
|
||||
import edu.wpi.first.cscore.UsbCamera;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
@@ -26,27 +28,55 @@ import org.photonvision.vision.processes.VisionSourceSettables;
|
||||
public class USBFrameProvider extends CpuImageProcessor {
|
||||
private final Logger logger;
|
||||
|
||||
private final CvSink cvSink;
|
||||
private UsbCamera camera = null;
|
||||
private CvSink cvSink = null;
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private VisionSourceSettables settables;
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
public USBFrameProvider(CvSink sink, VisionSourceSettables visionSettables) {
|
||||
logger = new Logger(USBFrameProvider.class, sink.getName(), LogGroup.Camera);
|
||||
private Runnable connectedCallback;
|
||||
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
public USBFrameProvider(
|
||||
UsbCamera camera, VisionSourceSettables visionSettables, Runnable connectedCallback) {
|
||||
this.camera = camera;
|
||||
this.cvSink = CameraServer.getVideo(this.camera);
|
||||
this.logger =
|
||||
new Logger(
|
||||
USBFrameProvider.class, visionSettables.getConfiguration().nickname, LogGroup.Camera);
|
||||
this.cvSink.setEnabled(true);
|
||||
|
||||
cvSink = sink;
|
||||
cvSink.setEnabled(true);
|
||||
this.settables = visionSettables;
|
||||
this.connectedCallback = connectedCallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkCameraConnected() {
|
||||
boolean connected = camera.isConnected();
|
||||
|
||||
if (!cameraPropertiesCached && connected) {
|
||||
logger.info("Camera connected! running callback");
|
||||
onCameraConnected();
|
||||
}
|
||||
|
||||
return connected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CapturedFrame getInputMat() {
|
||||
// We allocate memory so we don't fill a Mat in use by another thread (memory model is easier)
|
||||
if (!cameraPropertiesCached && camera.isConnected()) {
|
||||
onCameraConnected();
|
||||
}
|
||||
|
||||
// We allocate memory so we don't fill a Mat in use by another thread (memory
|
||||
// model is easier)
|
||||
var mat = new CVMat();
|
||||
// This is from wpi::Now, or WPIUtilJNI.now(). The epoch from grabFrame is uS since
|
||||
// Hal::initialize was called
|
||||
long captureTimeNs = cvSink.grabFrame(mat.getMat()) * 1000;
|
||||
// This is from wpi::Now, or WPIUtilJNI.now(). The epoch from grabFrame is uS
|
||||
// since
|
||||
// Hal::initialize was called. Timeout is in seconds
|
||||
// TODO - under the hood, this incurs an extra copy. We should avoid this, if we
|
||||
// can.
|
||||
long captureTimeNs = cvSink.grabFrame(mat.getMat(), 1.0) * 1000;
|
||||
|
||||
if (captureTimeNs == 0) {
|
||||
var error = cvSink.getError();
|
||||
@@ -61,6 +91,25 @@ public class USBFrameProvider extends CpuImageProcessor {
|
||||
return "USBFrameProvider - " + cvSink.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
CameraServer.removeServer(cvSink.getName());
|
||||
cvSink.close();
|
||||
cvSink = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraConnected() {
|
||||
super.onCameraConnected();
|
||||
|
||||
this.connectedCallback.run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return camera.isConnected();
|
||||
}
|
||||
|
||||
public void updateSettables(VisionSourceSettables settables) {
|
||||
this.settables = settables;
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import java.util.List;
|
||||
import org.photonvision.vision.camera.CameraType;
|
||||
|
||||
public class CameraMatchingOptions {
|
||||
public CameraMatchingOptions(
|
||||
boolean checkUSBPath,
|
||||
boolean checkVidPid,
|
||||
boolean checkBaseName,
|
||||
boolean checkPath,
|
||||
CameraType... allowedTypes) {
|
||||
this.checkUSBPath = checkUSBPath;
|
||||
this.checkVidPid = checkVidPid;
|
||||
this.checkBaseName = checkBaseName;
|
||||
this.checkPath = checkPath;
|
||||
this.allowedTypes = List.of(allowedTypes);
|
||||
}
|
||||
|
||||
public final boolean checkUSBPath;
|
||||
public final boolean checkVidPid;
|
||||
public final boolean checkBaseName;
|
||||
public final boolean checkPath;
|
||||
public final List<CameraType> allowedTypes;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CameraMatchingOptions [checkUSBPath="
|
||||
+ checkUSBPath
|
||||
+ ", checkVidPid="
|
||||
+ checkVidPid
|
||||
+ ", checkBaseName="
|
||||
+ checkBaseName
|
||||
+ ", checkPath="
|
||||
+ checkPath
|
||||
+ ", allowedTypes="
|
||||
+ allowedTypes
|
||||
+ "]";
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) Photon Vision.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
import org.photonvision.vision.pipeline.result.CVPipelineResult;
|
||||
|
||||
// TODO replace with CTT's data class
|
||||
public class Data implements Releasable {
|
||||
public CVPipelineResult result;
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
result.release();
|
||||
}
|
||||
}
|
||||
@@ -26,12 +26,12 @@ import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
import org.opencv.core.Size;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.DataChangeService.SubscriberHandle;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.networktables.NTDataPublisher;
|
||||
import org.photonvision.common.dataflow.statusLEDs.StatusLEDConsumer;
|
||||
@@ -45,8 +45,8 @@ import org.photonvision.common.util.SerializationUtils;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.camera.CameraType;
|
||||
import org.photonvision.vision.camera.LibcameraGpuSource;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.camera.csi.LibcameraGpuSource;
|
||||
import org.photonvision.vision.frame.Frame;
|
||||
import org.photonvision.vision.frame.consumer.FileSaveFrameConsumer;
|
||||
import org.photonvision.vision.frame.consumer.MJPGFrameConsumer;
|
||||
@@ -71,6 +71,7 @@ public class VisionModule {
|
||||
private final VisionRunner visionRunner;
|
||||
private final StreamRunnable streamRunnable;
|
||||
private final VisionModuleChangeSubscriber changeSubscriber;
|
||||
private final SubscriberHandle changeSubscriberHandle;
|
||||
private final LinkedList<CVPipelineResultConsumer> resultConsumers = new LinkedList<>();
|
||||
// Raw result consumers run before any drawing has been done by the
|
||||
// OutputStreamPipeline
|
||||
@@ -134,7 +135,7 @@ public class VisionModule {
|
||||
this.streamRunnable = new StreamRunnable(new OutputStreamPipeline());
|
||||
this.moduleIndex = index;
|
||||
|
||||
DataChangeService.getInstance().addSubscriber(changeSubscriber);
|
||||
changeSubscriberHandle = DataChangeService.getInstance().addSubscriber(changeSubscriber);
|
||||
|
||||
createStreams();
|
||||
|
||||
@@ -258,7 +259,7 @@ public class VisionModule {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (true) {
|
||||
while (!Thread.interrupted()) {
|
||||
final Frame m_frame;
|
||||
final AdvancedPipelineSettings settings;
|
||||
final List<TrackedTarget> targets;
|
||||
@@ -292,7 +293,8 @@ public class VisionModule {
|
||||
try {
|
||||
Thread.sleep(1);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
logger.warn("StreamRunnable was interrupted - exiting");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,10 +302,33 @@ public class VisionModule {
|
||||
}
|
||||
|
||||
public void start() {
|
||||
visionSource.cameraConfiguration.deactivated = false;
|
||||
visionRunner.startProcess();
|
||||
streamRunnable.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
visionSource.cameraConfiguration.deactivated = true;
|
||||
visionRunner.stopProcess();
|
||||
|
||||
try {
|
||||
streamRunnable.interrupt();
|
||||
streamRunnable.join();
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("Exception killing process thread", e);
|
||||
}
|
||||
|
||||
visionSource.release();
|
||||
|
||||
inputVideoStreamer.close();
|
||||
outputVideoStreamer.close();
|
||||
inputFrameSaver.close();
|
||||
outputFrameSaver.close();
|
||||
|
||||
changeSubscriberHandle.stop();
|
||||
setVisionLEDs(false);
|
||||
}
|
||||
|
||||
public void setFov(double fov) {
|
||||
var settables = visionSource.getSettables();
|
||||
logger.trace(() -> "Setting " + settables.getConfiguration().nickname + ") FOV (" + fov + ")");
|
||||
@@ -411,6 +436,9 @@ public class VisionModule {
|
||||
logger.info("Setting pipeline to " + index);
|
||||
logger.info("Pipeline name: " + pipelineManager.getPipelineNickname(index));
|
||||
pipelineManager.setIndex(index);
|
||||
|
||||
VisionSourceSettables settables = visionSource.getSettables();
|
||||
|
||||
var pipelineSettings = pipelineManager.getPipelineSettings(index);
|
||||
|
||||
if (pipelineSettings == null) {
|
||||
@@ -418,49 +446,51 @@ public class VisionModule {
|
||||
return false;
|
||||
}
|
||||
|
||||
visionSource.getSettables().setVideoModeInternal(pipelineSettings.cameraVideoModeIndex);
|
||||
visionSource.getSettables().setBrightness(pipelineSettings.cameraBrightness);
|
||||
visionRunner.runSyncronously(
|
||||
() -> {
|
||||
settables.setVideoModeInternal(pipelineSettings.cameraVideoModeIndex);
|
||||
settables.setBrightness(pipelineSettings.cameraBrightness);
|
||||
|
||||
// If manual exposure, force exposure slider to be valid
|
||||
if (!pipelineSettings.cameraAutoExposure) {
|
||||
if (pipelineSettings.cameraExposureRaw < 0)
|
||||
pipelineSettings.cameraExposureRaw = 10; // reasonable default
|
||||
}
|
||||
// If manual exposure, force exposure slider to be valid
|
||||
if (!pipelineSettings.cameraAutoExposure) {
|
||||
if (pipelineSettings.cameraExposureRaw < 0)
|
||||
pipelineSettings.cameraExposureRaw = 10; // reasonable default
|
||||
}
|
||||
|
||||
visionSource.getSettables().setExposureRaw(pipelineSettings.cameraExposureRaw);
|
||||
try {
|
||||
visionSource.getSettables().setAutoExposure(pipelineSettings.cameraAutoExposure);
|
||||
} catch (VideoException e) {
|
||||
logger.error("Unable to set camera auto exposure!");
|
||||
logger.error(e.toString());
|
||||
}
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
|
||||
// If the gain is disabled for some reason, re-enable it
|
||||
if (pipelineSettings.cameraGain == -1) pipelineSettings.cameraGain = 75;
|
||||
visionSource.getSettables().setGain(Math.max(0, pipelineSettings.cameraGain));
|
||||
} else {
|
||||
pipelineSettings.cameraGain = -1;
|
||||
}
|
||||
settables.setExposureRaw(pipelineSettings.cameraExposureRaw);
|
||||
try {
|
||||
settables.setAutoExposure(pipelineSettings.cameraAutoExposure);
|
||||
} catch (VideoException e) {
|
||||
logger.error("Unable to set camera auto exposure!");
|
||||
logger.error(e.toString());
|
||||
}
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.Gain)) {
|
||||
// If the gain is disabled for some reason, re-enable it
|
||||
if (pipelineSettings.cameraGain == -1) pipelineSettings.cameraGain = 75;
|
||||
settables.setGain(Math.max(0, pipelineSettings.cameraGain));
|
||||
} else {
|
||||
pipelineSettings.cameraGain = -1;
|
||||
}
|
||||
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.AwbRedBlueGain)) {
|
||||
// If the AWB gains are disabled for some reason, re-enable it
|
||||
if (pipelineSettings.cameraRedGain == -1) pipelineSettings.cameraRedGain = 11;
|
||||
if (pipelineSettings.cameraBlueGain == -1) pipelineSettings.cameraBlueGain = 20;
|
||||
visionSource.getSettables().setRedGain(Math.max(0, pipelineSettings.cameraRedGain));
|
||||
visionSource.getSettables().setBlueGain(Math.max(0, pipelineSettings.cameraBlueGain));
|
||||
} else {
|
||||
pipelineSettings.cameraRedGain = -1;
|
||||
pipelineSettings.cameraBlueGain = -1;
|
||||
if (cameraQuirks.hasQuirk(CameraQuirk.AwbRedBlueGain)) {
|
||||
// If the AWB gains are disabled for some reason, re-enable it
|
||||
if (pipelineSettings.cameraRedGain == -1) pipelineSettings.cameraRedGain = 11;
|
||||
if (pipelineSettings.cameraBlueGain == -1) pipelineSettings.cameraBlueGain = 20;
|
||||
settables.setRedGain(Math.max(0, pipelineSettings.cameraRedGain));
|
||||
settables.setBlueGain(Math.max(0, pipelineSettings.cameraBlueGain));
|
||||
} else {
|
||||
pipelineSettings.cameraRedGain = -1;
|
||||
pipelineSettings.cameraBlueGain = -1;
|
||||
|
||||
// All other cameras (than picams) should support AWB temp
|
||||
visionSource.getSettables().setWhiteBalanceTemp(pipelineSettings.cameraWhiteBalanceTemp);
|
||||
visionSource.getSettables().setAutoWhiteBalance(pipelineSettings.cameraAutoWhiteBalance);
|
||||
}
|
||||
// All other cameras (than picams) should support AWB temp
|
||||
settables.setWhiteBalanceTemp(pipelineSettings.cameraWhiteBalanceTemp);
|
||||
settables.setAutoWhiteBalance(pipelineSettings.cameraAutoWhiteBalance);
|
||||
}
|
||||
|
||||
setVisionLEDs(pipelineSettings.ledMode);
|
||||
setVisionLEDs(pipelineSettings.ledMode);
|
||||
|
||||
visionSource.getSettables().getConfiguration().currentPipelineIndex =
|
||||
pipelineManager.getRequestedIndex();
|
||||
settables.getConfiguration().currentPipelineIndex = pipelineManager.getRequestedIndex();
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -485,7 +515,7 @@ public class VisionModule {
|
||||
getStateAsCameraConfig(), visionSource.getSettables().getConfiguration().uniqueName);
|
||||
}
|
||||
|
||||
void saveAndBroadcastAll() {
|
||||
public void saveAndBroadcastAll() {
|
||||
saveModule();
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
@@ -521,8 +551,11 @@ public class VisionModule {
|
||||
public UICameraConfiguration toUICameraConfig() {
|
||||
var ret = new UICameraConfiguration();
|
||||
|
||||
var config = visionSource.getCameraConfiguration();
|
||||
ret.matchedCameraInfo = config.matchedCameraInfo;
|
||||
ret.cameraPath = config.getDevicePath();
|
||||
ret.fov = visionSource.getSettables().getFOV();
|
||||
ret.isCSICamera = visionSource.getCameraConfiguration().cameraType == CameraType.ZeroCopyPicam;
|
||||
ret.isCSICamera = config.matchedCameraInfo.type() == CameraType.ZeroCopyPicam;
|
||||
ret.nickname = visionSource.getSettables().getConfiguration().nickname;
|
||||
ret.uniqueName = visionSource.getSettables().getConfiguration().uniqueName;
|
||||
ret.currentPipelineSettings =
|
||||
@@ -535,6 +568,8 @@ public class VisionModule {
|
||||
ret.minWhiteBalanceTemp = visionSource.getSettables().getMinWhiteBalanceTemp();
|
||||
ret.maxWhiteBalanceTemp = visionSource.getSettables().getMaxWhiteBalanceTemp();
|
||||
|
||||
ret.deactivated = config.deactivated;
|
||||
|
||||
// TODO refactor into helper method
|
||||
var temp = new HashMap<Integer, HashMap<String, Object>>();
|
||||
var videoModes = visionSource.getSettables().getAllVideoModes();
|
||||
@@ -554,6 +589,11 @@ public class VisionModule {
|
||||
|
||||
temp.put(k, internalMap);
|
||||
}
|
||||
|
||||
if (videoModes.size() == 0) {
|
||||
logger.error("no video modes, guhhhhh");
|
||||
}
|
||||
|
||||
ret.videoFormatList = temp;
|
||||
ret.outputStreamPort = this.outputStreamPort;
|
||||
ret.inputStreamPort = this.inputStreamPort;
|
||||
@@ -561,11 +601,14 @@ public class VisionModule {
|
||||
ret.calibrations =
|
||||
visionSource.getSettables().getConfiguration().calibrations.stream()
|
||||
.map(CameraCalibrationCoefficients::cloneWithoutObservations)
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
|
||||
ret.isFovConfigurable =
|
||||
!(ConfigManager.getInstance().getConfig().getHardwareConfig().hasPresetFOV());
|
||||
|
||||
ret.isConnected = visionSource.getFrameProvider().isConnected();
|
||||
ret.hasConnected = visionSource.getFrameProvider().hasConnected();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -643,4 +686,12 @@ public class VisionModule {
|
||||
visionSource.remakeSettables();
|
||||
saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
public String uniqueName() {
|
||||
return this.visionSource.cameraConfiguration.uniqueName;
|
||||
}
|
||||
|
||||
public CameraConfiguration getCameraConfiguration() {
|
||||
return this.visionSource.cameraConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.photonvision.vision.processes;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import org.photonvision.common.dataflow.websocket.UICameraConfiguration;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
|
||||
@@ -26,15 +27,7 @@ import org.photonvision.common.logging.Logger;
|
||||
public class VisionModuleManager {
|
||||
private final Logger logger = new Logger(VisionModuleManager.class, LogGroup.VisionModule);
|
||||
|
||||
private static class ThreadSafeSingleton {
|
||||
private static final VisionModuleManager INSTANCE = new VisionModuleManager();
|
||||
}
|
||||
|
||||
public static VisionModuleManager getInstance() {
|
||||
return VisionModuleManager.ThreadSafeSingleton.INSTANCE;
|
||||
}
|
||||
|
||||
protected final List<VisionModule> visionModules = new ArrayList<>();
|
||||
private final List<VisionModule> visionModules = new ArrayList<>();
|
||||
|
||||
VisionModuleManager() {}
|
||||
|
||||
@@ -53,25 +46,25 @@ public class VisionModuleManager {
|
||||
return visionModules.get(i);
|
||||
}
|
||||
|
||||
public List<VisionModule> addSources(List<VisionSource> visionSources) {
|
||||
var addedModules = new HashMap<Integer, VisionModule>();
|
||||
public synchronized VisionModule addSource(VisionSource visionSource) {
|
||||
visionSource.cameraConfiguration.streamIndex = newCameraIndex();
|
||||
|
||||
assignCameraIndex(visionSources);
|
||||
for (var visionSource : visionSources) {
|
||||
var pipelineManager = new PipelineManager(visionSource.getCameraConfiguration());
|
||||
var pipelineManager = new PipelineManager(visionSource.getCameraConfiguration());
|
||||
var module =
|
||||
new VisionModule(
|
||||
pipelineManager, visionSource, visionSource.cameraConfiguration.streamIndex);
|
||||
visionModules.add(module);
|
||||
|
||||
var module = new VisionModule(pipelineManager, visionSource, visionModules.size());
|
||||
visionModules.add(module);
|
||||
addedModules.put(visionSource.getCameraConfiguration().streamIndex, module);
|
||||
}
|
||||
|
||||
return addedModules.entrySet().stream()
|
||||
.sorted(Comparator.comparingInt(Map.Entry::getKey)) // sort by stream index
|
||||
.map(Map.Entry::getValue) // map to Stream of VisionModule
|
||||
.collect(Collectors.toList()); // collect in a List
|
||||
return module;
|
||||
}
|
||||
|
||||
private void assignCameraIndex(List<VisionSource> config) {
|
||||
public synchronized void removeModule(VisionModule module) {
|
||||
visionModules.remove(module);
|
||||
module.stop();
|
||||
module.saveAndBroadcastAll();
|
||||
}
|
||||
|
||||
private synchronized int newCameraIndex() {
|
||||
// We won't necessarily have already added all the cameras we need to at this point
|
||||
// But by operating on the list, we have a fairly good idea of which we need to change,
|
||||
// but it's not guaranteed that we change the correct one
|
||||
@@ -80,29 +73,40 @@ public class VisionModuleManager {
|
||||
|
||||
// Big list, which should contain every vision source (currently loaded plus the new ones being
|
||||
// added)
|
||||
var bigList = new ArrayList<VisionSource>();
|
||||
bigList.addAll(
|
||||
this.getModules().stream().map(it -> it.visionSource).collect(Collectors.toList()));
|
||||
bigList.addAll(config);
|
||||
|
||||
for (var v : config) {
|
||||
var listNoV = new ArrayList<>(bigList);
|
||||
listNoV.remove(v);
|
||||
if (listNoV.stream()
|
||||
.anyMatch(
|
||||
it ->
|
||||
it.getCameraConfiguration().streamIndex
|
||||
== v.getCameraConfiguration().streamIndex)) {
|
||||
int idx = 0;
|
||||
while (listNoV.stream()
|
||||
List<Integer> bigList =
|
||||
this.getModules().stream()
|
||||
.map(it -> it.getCameraConfiguration().streamIndex)
|
||||
.collect(Collectors.toList())
|
||||
.contains(idx)) {
|
||||
idx++;
|
||||
}
|
||||
logger.debug("Assigning idx " + idx);
|
||||
v.getCameraConfiguration().streamIndex = idx;
|
||||
}
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int idx = 0;
|
||||
while (bigList.contains(idx)) {
|
||||
idx++;
|
||||
}
|
||||
|
||||
if (idx >= 5) {
|
||||
logger.warn("VisionModuleManager has reached the maximum number of cameras (5).");
|
||||
}
|
||||
|
||||
return idx;
|
||||
}
|
||||
|
||||
public static class UiVmmState {
|
||||
public final List<UICameraConfiguration> visionModules;
|
||||
|
||||
UiVmmState(List<UICameraConfiguration> _v) {
|
||||
this.visionModules = _v;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized UiVmmState getState() {
|
||||
return new UiVmmState(
|
||||
this.visionModules.stream()
|
||||
.map(VisionModule::toUICameraConfig)
|
||||
.map(
|
||||
it -> {
|
||||
it.calibrations = null;
|
||||
return it;
|
||||
})
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,17 @@
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.DataChangeService;
|
||||
import org.photonvision.common.dataflow.events.OutgoingUIEvent;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
@@ -38,6 +47,7 @@ public class VisionRunner {
|
||||
private final Supplier<CVPipeline> pipelineSupplier;
|
||||
private final Consumer<CVPipelineResult> pipelineResultConsumer;
|
||||
private final VisionModuleChangeSubscriber changeSubscriber;
|
||||
private final List<Runnable> runnableList = new ArrayList<Runnable>();
|
||||
private final QuirkyCamera cameraQuirks;
|
||||
|
||||
private long loopCount;
|
||||
@@ -72,12 +82,86 @@ public class VisionRunner {
|
||||
visionProcessThread.start();
|
||||
}
|
||||
|
||||
public void stopProcess() {
|
||||
try {
|
||||
System.out.println("Interrupting vision process thread");
|
||||
visionProcessThread.interrupt();
|
||||
visionProcessThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("Exception killing process thread", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Future<Void> runSyncronously(Runnable runnable) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
synchronized (runnableList) {
|
||||
runnableList.add(
|
||||
() -> {
|
||||
try {
|
||||
runnable.run();
|
||||
future.complete(null);
|
||||
} catch (Exception ex) {
|
||||
future.completeExceptionally(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
return future;
|
||||
}
|
||||
|
||||
public <T> Future<T> runSyncronously(Callable<T> callable) {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
|
||||
synchronized (runnableList) {
|
||||
runnableList.add(
|
||||
() -> {
|
||||
try {
|
||||
T result = callable.call();
|
||||
future.complete(result);
|
||||
} catch (Exception ex) {
|
||||
future.completeExceptionally(ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
private void update() {
|
||||
// wait for the camera to connect
|
||||
while (!frameSupplier.checkCameraConnected() && !Thread.interrupted()) {
|
||||
// yield
|
||||
pipelineResultConsumer.accept(new CVPipelineResult(0l, 0, 0, null, new Frame()));
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
|
||||
while (!Thread.interrupted()) {
|
||||
changeSubscriber.processSettingChanges();
|
||||
synchronized (runnableList) {
|
||||
for (var runnable : runnableList) {
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Exception ex) {
|
||||
logger.error("Exception running runnable", ex);
|
||||
}
|
||||
}
|
||||
runnableList.clear();
|
||||
}
|
||||
|
||||
var pipeline = pipelineSupplier.get();
|
||||
|
||||
// Tell our camera implementation here what kind of pre-processing we need it to be doing
|
||||
// Tell our camera implementation here what kind of pre-processing we need it to
|
||||
// be doing
|
||||
// (pipeline-dependent). I kinda hate how much leak this has...
|
||||
// TODO would a callback object be a better fit?
|
||||
var wantedProcessType = pipeline.getThresholdType();
|
||||
@@ -101,14 +185,17 @@ public class VisionRunner {
|
||||
if (frame.processedImage.getMat().empty() && frame.colorImage.getMat().empty()) {
|
||||
// give up without increasing loop count
|
||||
// Still feed with blank frames just dont run any pipelines
|
||||
|
||||
pipelineResultConsumer.accept(new CVPipelineResult(0l, 0, 0, null, new Frame()));
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the pipeline has changed while we are getting our frame we should scrap that frame it
|
||||
// If the pipeline has changed while we are getting our frame we should scrap
|
||||
// that frame it
|
||||
// may result in incorrect frame settings like hsv values
|
||||
if (pipeline == pipelineSupplier.get()) {
|
||||
// There's no guarantee the processing type change will occur this tick, so pipelines should
|
||||
// There's no guarantee the processing type change will occur this tick, so
|
||||
// pipelines should
|
||||
// check themselves
|
||||
try {
|
||||
var pipelineResult = pipeline.run(frame, cameraQuirks);
|
||||
|
||||
@@ -19,8 +19,9 @@ package org.photonvision.vision.processes;
|
||||
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
import org.photonvision.vision.opencv.Releasable;
|
||||
|
||||
public abstract class VisionSource {
|
||||
public abstract class VisionSource implements Releasable {
|
||||
protected final CameraConfiguration cameraConfiguration;
|
||||
|
||||
protected VisionSource(CameraConfiguration cameraConfiguration) {
|
||||
|
||||
@@ -21,14 +21,17 @@ import edu.wpi.first.cscore.UsbCamera;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
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.websocket.UICameraConfiguration;
|
||||
import org.photonvision.common.dataflow.websocket.UIPhotonConfiguration;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.hardware.Platform.OSType;
|
||||
@@ -37,24 +40,28 @@ import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TimedTaskManager;
|
||||
import org.photonvision.raspi.LibCameraJNI;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
import org.photonvision.vision.camera.CameraInfo;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.camera.CameraType;
|
||||
import org.photonvision.vision.camera.LibcameraGpuSource;
|
||||
import org.photonvision.vision.camera.TestSource;
|
||||
import org.photonvision.vision.camera.FileVisionSource;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
import org.photonvision.vision.camera.USBCameras.USBCameraSource;
|
||||
import org.photonvision.vision.camera.csi.LibcameraGpuSource;
|
||||
|
||||
/**
|
||||
* This class manages starting up VisionModules for serialized devices ({@link
|
||||
* VisionSourceManager#loadVisionSourceFromCamConfig}), as well as handling requests from users to
|
||||
* disable (release the camera device, but keep the configuration around) ({@link
|
||||
* VisionSourceManager#deactivateVisionSource}), reactivate (recreate a VisionModule from a saved
|
||||
* and currently disabled configuration) ({@link
|
||||
* VisionSourceManager#reactivateDisabledCameraConfig}), and create a new VisionModule from a {@link
|
||||
* PVCameraInfo} ({@link VisionSourceManager#assignUnmatchedCamera}).
|
||||
*
|
||||
* <p>We now require user interaction for pretty much every operation this undertakes.
|
||||
*/
|
||||
public class VisionSourceManager {
|
||||
private static final Logger logger = new Logger(VisionSourceManager.class, LogGroup.Camera);
|
||||
|
||||
private static final List<String> deviceBlacklist = List.of("bcm2835-isp");
|
||||
|
||||
final List<CameraInfo> knownCameras = new CopyOnWriteArrayList<>();
|
||||
|
||||
final List<CameraConfiguration> unmatchedLoadedConfigs = new CopyOnWriteArrayList<>();
|
||||
private boolean hasWarned;
|
||||
private boolean hasWarnedNoCameras = false;
|
||||
private String ignoredCamerasRegex = "";
|
||||
|
||||
private static class SingletonHolder {
|
||||
private static final VisionSourceManager INSTANCE = new VisionSourceManager();
|
||||
}
|
||||
@@ -63,547 +70,348 @@ public class VisionSourceManager {
|
||||
return SingletonHolder.INSTANCE;
|
||||
}
|
||||
|
||||
VisionSourceManager() {}
|
||||
|
||||
public void registerTimedTask() {
|
||||
TimedTaskManager.getInstance().addTask("VisionSourceManager", this::tryMatchCams, 3000);
|
||||
// Jackson does use these members even if your IDE claims otherwise
|
||||
public static class VisionSourceManagerState {
|
||||
public List<UICameraConfiguration> disabledConfigs;
|
||||
public List<PVCameraInfo> allConnectedCameras;
|
||||
}
|
||||
|
||||
public void registerLoadedConfigs(CameraConfiguration... configs) {
|
||||
registerLoadedConfigs(Arrays.asList(configs));
|
||||
// Map of (unique name) -> (all CameraConfigurations) that have been registered
|
||||
protected final HashMap<String, CameraConfiguration> disabledCameraConfigs = new HashMap<>();
|
||||
|
||||
// The subset of cameras that are "active", converted to VisionModules
|
||||
public VisionModuleManager vmm = new VisionModuleManager();
|
||||
|
||||
public void registerTimedTasks() {
|
||||
TimedTaskManager.getInstance().addTask("CameraDeviceExplorer", this::pushUiUpdate, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register new camera configs loaded from disk. This will add them to the list of configs to try
|
||||
* to match, and also automatically spawn new vision processes as necessary.
|
||||
* Register new camera configs loaded from disk. This will create vision modules for each camera
|
||||
* config and start them.
|
||||
*
|
||||
* @param configs The loaded camera configs.
|
||||
*/
|
||||
public void registerLoadedConfigs(Collection<CameraConfiguration> configs) {
|
||||
unmatchedLoadedConfigs.addAll(configs);
|
||||
}
|
||||
public synchronized void registerLoadedConfigs(Collection<CameraConfiguration> configs) {
|
||||
logger.info("Registering loaded camera configs");
|
||||
|
||||
/**
|
||||
* Pre filter out any csi cameras to return just USB Cameras. Allow defining the camerainfo.
|
||||
*
|
||||
* @return a list containing usbcamerainfo.
|
||||
*/
|
||||
protected List<CameraInfo> getConnectedUSBCameras() {
|
||||
List<CameraInfo> cameraInfos =
|
||||
List.of(UsbCamera.enumerateUsbCameras()).stream()
|
||||
.map(c -> new CameraInfo(c))
|
||||
.collect(Collectors.toList());
|
||||
return cameraInfos;
|
||||
}
|
||||
final HashMap<String, CameraConfiguration> deserializedConfigs = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Retrieve the list of csi cameras from libcamera.
|
||||
*
|
||||
* @return a list containing csicamerainfo.
|
||||
*/
|
||||
protected List<CameraInfo> getConnectedCSICameras() {
|
||||
List<CameraInfo> cameraInfos = new ArrayList<CameraInfo>();
|
||||
if (LibCameraJNILoader.isSupported())
|
||||
for (String path : LibCameraJNI.getCameraNames()) {
|
||||
String name = LibCameraJNI.getSensorModel(path).getFriendlyName();
|
||||
cameraInfos.add(
|
||||
new CameraInfo(-1, path, name, new String[] {}, -1, -1, CameraType.ZeroCopyPicam));
|
||||
// 1. Verify all camera unique names are unique and paths/types are unique for paranoia. This
|
||||
// seems redundant, consider deleting
|
||||
for (var config : configs) {
|
||||
Predicate<PVCameraInfo> checkDuplicateCamera =
|
||||
(other) ->
|
||||
(other.type().equals(config.matchedCameraInfo.type())
|
||||
&& other.uniquePath().equals(config.matchedCameraInfo.uniquePath()));
|
||||
|
||||
if (deserializedConfigs.containsKey(config.uniqueName)) {
|
||||
logger.error(
|
||||
"Duplicate unique name for config " + config.uniqueName + " -- not overwriting");
|
||||
} else if (deserializedConfigs.values().stream()
|
||||
.map(it -> it.matchedCameraInfo)
|
||||
.anyMatch(checkDuplicateCamera)) {
|
||||
logger.error(
|
||||
"Duplicate camera type & path for config " + config.uniqueName + " -- not overwriting");
|
||||
} else {
|
||||
deserializedConfigs.put(config.uniqueName, config);
|
||||
}
|
||||
return cameraInfos;
|
||||
}
|
||||
|
||||
// 2. create sources -> VMMs for all active cameras and add to our VMM. We don't care about if
|
||||
// the underlying device is currently connected or not.
|
||||
deserializedConfigs.values().stream()
|
||||
.filter(it -> !it.deactivated)
|
||||
.map(this::loadVisionSourceFromCamConfig)
|
||||
.map(vmm::addSource)
|
||||
.forEach(VisionModule::start);
|
||||
|
||||
// 3. write down all disabled sources for later
|
||||
deserializedConfigs.entrySet().stream()
|
||||
.filter(it -> it.getValue().deactivated)
|
||||
.forEach(it -> this.disabledCameraConfigs.put(it.getKey(), it.getValue()));
|
||||
|
||||
logger.info(
|
||||
"Finished registering loaded camera configs! Started "
|
||||
+ vmm.getModules().size()
|
||||
+ " active VisionModules, with "
|
||||
+ deserializedConfigs.size()
|
||||
+ " disabled VisionModules");
|
||||
}
|
||||
|
||||
protected void tryMatchCams() {
|
||||
var visionSourceList = tryMatchCamImpl();
|
||||
if (visionSourceList == null) return;
|
||||
/**
|
||||
* Reactivate a previously created vision source
|
||||
*
|
||||
* @param uniqueName
|
||||
*/
|
||||
public synchronized boolean reactivateDisabledCameraConfig(String uniqueName) {
|
||||
// Make sure we have an old, currently -inactive- camera around
|
||||
var deactivatedConfig = Optional.ofNullable(this.disabledCameraConfigs.remove(uniqueName));
|
||||
if (deactivatedConfig.isEmpty() || !deactivatedConfig.get().deactivated) {
|
||||
// Not in map, give up
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info("Adding " + visionSourceList.size() + " configs to VMM.");
|
||||
ConfigManager.getInstance().addCameraConfigurations(visionSourceList);
|
||||
var addedSources = VisionModuleManager.getInstance().addSources(visionSourceList);
|
||||
addedSources.forEach(VisionModule::start);
|
||||
// Check if the camera is already in use by another module
|
||||
if (vmm.getModules().stream()
|
||||
.anyMatch(
|
||||
module ->
|
||||
module
|
||||
.getCameraConfiguration()
|
||||
.matchedCameraInfo
|
||||
.uniquePath()
|
||||
.equals(deactivatedConfig.get().matchedCameraInfo.uniquePath()))) {
|
||||
logger.error(
|
||||
"Camera unique-path already in use by active VisionModule! Cannot reactivate "
|
||||
+ deactivatedConfig.get().nickname);
|
||||
}
|
||||
|
||||
// transform the camera info all the way to a VisionModule and then start it
|
||||
var created =
|
||||
deactivatedConfig
|
||||
.map(this::loadVisionSourceFromCamConfig)
|
||||
.map(vmm::addSource)
|
||||
.map(
|
||||
it -> {
|
||||
it.start();
|
||||
it.saveAndBroadcastAll();
|
||||
return it;
|
||||
})
|
||||
.isPresent();
|
||||
|
||||
if (!created) {
|
||||
// Couldn't create a VM for this config - restore state
|
||||
this.disabledCameraConfigs.put(uniqueName, deactivatedConfig.get());
|
||||
}
|
||||
|
||||
// We have a new camera! Tell the world about it
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
}
|
||||
|
||||
protected List<VisionSource> tryMatchCamImpl() {
|
||||
return tryMatchCamImpl(null);
|
||||
}
|
||||
pushUiUpdate();
|
||||
|
||||
protected List<VisionSource> tryMatchCamImpl(ArrayList<CameraInfo> cameraInfos) {
|
||||
return tryMatchCamImpl(cameraInfos, Platform.getCurrentPlatform());
|
||||
return created;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cameraInfos Used to feed camera info for unit tests.
|
||||
* @return New VisionSources.
|
||||
*/
|
||||
protected List<VisionSource> tryMatchCamImpl(
|
||||
ArrayList<CameraInfo> cameraInfos, Platform platform) {
|
||||
boolean createSources = true;
|
||||
List<CameraInfo> connectedCameras;
|
||||
if (cameraInfos == null) {
|
||||
// Detect USB cameras using CSCore
|
||||
connectedCameras = new ArrayList<>(filterAllowedDevices(getConnectedUSBCameras(), platform));
|
||||
// Detect CSI cameras using libcamera
|
||||
connectedCameras.addAll(
|
||||
new ArrayList<>(filterAllowedDevices(getConnectedCSICameras(), platform)));
|
||||
} else {
|
||||
connectedCameras = new ArrayList<>(filterAllowedDevices(cameraInfos, platform));
|
||||
createSources =
|
||||
false; // Dont create sources if we are using supplied camerainfo for unit tests.
|
||||
}
|
||||
|
||||
// Return no new sources because there are no new sources
|
||||
if (connectedCameras.isEmpty()) {
|
||||
if (!hasWarnedNoCameras) {
|
||||
logger.warn(
|
||||
"No cameras were detected! Check that all cameras are connected, and that the path is correct.");
|
||||
hasWarnedNoCameras = true;
|
||||
}
|
||||
return null;
|
||||
} else hasWarnedNoCameras = false;
|
||||
|
||||
// Remove any known cameras.
|
||||
connectedCameras.removeIf(c -> knownCameras.contains(c));
|
||||
|
||||
// All cameras are already loaded return no new sources.
|
||||
if (connectedCameras.isEmpty()) return null;
|
||||
|
||||
logger.debug("Matching " + connectedCameras.size() + " new camera(s)!");
|
||||
|
||||
// Debug prints
|
||||
for (var info : connectedCameras) {
|
||||
logger.info("Detected unmatched physical camera: " + info.toString());
|
||||
}
|
||||
|
||||
if (!unmatchedLoadedConfigs.isEmpty())
|
||||
logger.debug("Trying to match " + unmatchedLoadedConfigs.size() + " unmatched config(s)...");
|
||||
|
||||
// Match camera configs to physical cameras
|
||||
List<CameraConfiguration> matchedCameras =
|
||||
matchCameras(connectedCameras, unmatchedLoadedConfigs);
|
||||
|
||||
unmatchedLoadedConfigs.removeAll(matchedCameras);
|
||||
if (!unmatchedLoadedConfigs.isEmpty() && !hasWarned) {
|
||||
logger.warn(
|
||||
() ->
|
||||
"After matching, "
|
||||
+ unmatchedLoadedConfigs.size()
|
||||
+ " config(s) remained unmatched. Is your camera disconnected?");
|
||||
logger.warn(
|
||||
"Unloaded configs: "
|
||||
+ unmatchedLoadedConfigs.stream()
|
||||
.map(it -> it.nickname)
|
||||
.collect(Collectors.joining(", ")));
|
||||
hasWarned = true;
|
||||
}
|
||||
|
||||
// We add the matched cameras to the known camera list
|
||||
this.knownCameras.addAll(connectedCameras);
|
||||
|
||||
if (matchedCameras.isEmpty()) return null;
|
||||
|
||||
// Turn these camera configs into vision sources
|
||||
var sources = loadVisionSourcesFromCamConfigs(matchedCameras, createSources);
|
||||
|
||||
// Print info about each vision source
|
||||
for (var src : sources) {
|
||||
logger.debug(
|
||||
() ->
|
||||
"Matched config for camera \""
|
||||
+ src.getFrameProvider().getName()
|
||||
+ "\" and loaded "
|
||||
+ src.getCameraConfiguration().pipelineSettings.size()
|
||||
+ " pipelines");
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a predicate for checking cameras against a saved config.
|
||||
* Assign a camera that currently has no associated CameraConfiguration loaded.
|
||||
*
|
||||
* @param savedConfig The saved camera configuration to match against
|
||||
* @param checkUSBPath If we should compare the USB port/bus IDs
|
||||
* @param checkVidPid If we should compare USB VID and PID
|
||||
* @param checkBaseName If we should compare {@link CameraInfo#getBaseName}
|
||||
* @param checkPath If we should check {@link CameraInfo::path} (eg /dev/videoN on Linux, or
|
||||
* ?/usb#vid_05c8&pid_03df&mi_00#7&fa76035&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
|
||||
* on Windows)
|
||||
* @param cameraInfo
|
||||
*/
|
||||
private final Predicate<CameraInfo> getCameraMatcher(
|
||||
final CameraConfiguration savedConfig,
|
||||
boolean checkUSBPath,
|
||||
boolean checkVidPid,
|
||||
boolean checkBaseName,
|
||||
boolean checkPath) {
|
||||
if (checkUSBPath && savedConfig.getUSBPath().isEmpty()) {
|
||||
logger.debug(
|
||||
"WARN: Camera has empty USB path, but asked to match by name: "
|
||||
+ savedConfig.toShortString());
|
||||
public synchronized boolean assignUnmatchedCamera(PVCameraInfo cameraInfo) {
|
||||
// Check if the camera is already in use by another module
|
||||
if (vmm.getModules().stream()
|
||||
.anyMatch(
|
||||
module ->
|
||||
module
|
||||
.getCameraConfiguration()
|
||||
.matchedCameraInfo
|
||||
.uniquePath()
|
||||
.equals(cameraInfo.uniquePath()))) {
|
||||
logger.error(
|
||||
"Camera unique-path already in use by active VisionModule! Cannot add " + cameraInfo);
|
||||
}
|
||||
|
||||
return (CameraInfo physicalCamera) -> {
|
||||
var matches = true;
|
||||
var source = loadVisionSourceFromCamConfig(new CameraConfiguration(cameraInfo));
|
||||
var module = vmm.addSource(source);
|
||||
|
||||
if (checkUSBPath) {
|
||||
var savedPath = savedConfig.getUSBPath();
|
||||
matches &= (savedPath.isPresent() && physicalCamera.getUSBPath().equals(savedPath));
|
||||
}
|
||||
if (checkBaseName) {
|
||||
matches &= physicalCamera.getBaseName().equals(savedConfig.baseName);
|
||||
}
|
||||
if (checkVidPid) {
|
||||
matches &=
|
||||
(physicalCamera.vendorId == savedConfig.usbVID
|
||||
&& physicalCamera.productId == savedConfig.usbPID);
|
||||
}
|
||||
if (checkPath) {
|
||||
matches &= (physicalCamera.path.equals(savedConfig.path));
|
||||
}
|
||||
module.start();
|
||||
|
||||
matches &= (physicalCamera.cameraType == savedConfig.cameraType);
|
||||
// We have a new camera! Tell the world about it
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
|
||||
return matches;
|
||||
};
|
||||
pushUiUpdate();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
|
||||
* disk.
|
||||
*
|
||||
* @param detectedCamInfos Information about currently connected USB cameras.
|
||||
* @param loadedCamConfigs The USB {@link CameraConfiguration}s loaded from disk.
|
||||
* @return the matched configurations.
|
||||
*/
|
||||
public List<CameraConfiguration> matchCameras(
|
||||
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> loadedCamConfigs) {
|
||||
return matchCameras(
|
||||
detectedCamInfos,
|
||||
loadedCamConfigs,
|
||||
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath);
|
||||
public synchronized boolean deleteVisionSource(String uniqueName) {
|
||||
deactivateVisionSource(uniqueName);
|
||||
var config = disabledCameraConfigs.remove(uniqueName);
|
||||
ConfigManager.getInstance().getConfig().removeCameraConfig(uniqueName);
|
||||
ConfigManager.getInstance().saveToDisk();
|
||||
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(
|
||||
new OutgoingUIEvent<>(
|
||||
"fullsettings",
|
||||
UIPhotonConfiguration.programStateToUi(ConfigManager.getInstance().getConfig())));
|
||||
pushUiUpdate();
|
||||
|
||||
return config != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
|
||||
* disk.
|
||||
*
|
||||
* @param detectedCamInfos Information about currently connected USB cameras.
|
||||
* @param loadedCamConfigs The USB {@link CameraConfiguration}s loaded from disk.
|
||||
* @param matchCamerasOnlyByPath If we should never try to match only by (base name, vid, pid)
|
||||
* @return the matched configurations.
|
||||
*/
|
||||
public List<CameraConfiguration> matchCameras(
|
||||
List<CameraInfo> detectedCamInfos,
|
||||
List<CameraConfiguration> loadedCamConfigs,
|
||||
boolean matchCamerasOnlyByPath) {
|
||||
var detectedCameraList = new ArrayList<>(detectedCamInfos);
|
||||
ArrayList<CameraConfiguration> cameraConfigurations = new ArrayList<CameraConfiguration>();
|
||||
ArrayList<CameraConfiguration> unloadedConfigs =
|
||||
new ArrayList<CameraConfiguration>(loadedCamConfigs);
|
||||
public synchronized boolean deactivateVisionSource(String uniqueName) {
|
||||
// try to find the module. If we find it, remove it from the VMM
|
||||
var removedConfig =
|
||||
vmm.getModules().stream()
|
||||
.filter(module -> module.uniqueName().equals(uniqueName))
|
||||
.findFirst()
|
||||
.map(
|
||||
it -> {
|
||||
vmm.removeModule(it);
|
||||
return it.getCameraConfiguration();
|
||||
});
|
||||
|
||||
logger.info("Matching CSI cameras by port & base name...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(false, false, true, true, CameraType.ZeroCopyPicam)));
|
||||
|
||||
logger.info("Matching USB cameras by usb port & name & USB VID/PID...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(true, true, true, false, CameraType.UsbCamera)));
|
||||
|
||||
// On windows, the v4l path is actually useful and tells us the port the camera is physically
|
||||
// connected to which is neat
|
||||
if (Platform.isWindows() && !matchCamerasOnlyByPath) {
|
||||
logger.info("Matching USB cameras by windows-path & USB VID/PID only...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(false, true, true, true, CameraType.UsbCamera)));
|
||||
if (removedConfig.isEmpty()) {
|
||||
logger.error("Could not find module " + uniqueName);
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info("Matching USB cameras by usb port & USB VID/PID...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(true, true, false, false, CameraType.UsbCamera)));
|
||||
// And stuff it into our list of disabled camera configs
|
||||
disabledCameraConfigs.put(removedConfig.get().uniqueName, removedConfig.get());
|
||||
|
||||
// Legacy migration -- VID/PID will be unset, so we have to try with our most relaxed strategy
|
||||
// at least once. We _should_ still have a valid USB path (assuming cameras have not moved), so
|
||||
// try that first, then fallback to base name only beloow
|
||||
logger.info("Matching USB cameras by base-name & usb port...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(true, false, true, false, CameraType.UsbCamera)));
|
||||
logger.info("Disabled the VisionModule for " + removedConfig.get().nickname);
|
||||
|
||||
// handle disabling only-by-base-name matching
|
||||
if (!matchCamerasOnlyByPath) {
|
||||
logger.info("Matching USB cameras by base-name & USB VID/PID only...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(false, true, true, false, CameraType.UsbCamera)));
|
||||
pushUiUpdate();
|
||||
|
||||
// Legacy migration for if no USB VID/PID set
|
||||
logger.info("Matching USB cameras by base-name only...");
|
||||
cameraConfigurations.addAll(
|
||||
matchCamerasByStrategy(
|
||||
detectedCameraList,
|
||||
unloadedConfigs,
|
||||
new CameraMatchingOptions(false, false, true, false, CameraType.UsbCamera)));
|
||||
} else logger.info("Skipping match by filepath/vid/pid, disabled by user");
|
||||
|
||||
if (detectedCameraList.size() > 0) {
|
||||
// handle disabling only-by-base-name matching
|
||||
if (!matchCamerasOnlyByPath) {
|
||||
cameraConfigurations.addAll(
|
||||
createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations));
|
||||
} else {
|
||||
logger.warn(
|
||||
"Not creating 'new' Photon CameraConfigurations for ["
|
||||
+ detectedCamInfos.stream()
|
||||
.map(CameraInfo::toString)
|
||||
.collect(Collectors.joining(";"))
|
||||
+ "], disabled by user");
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Matched or created " + cameraConfigurations.size() + " camera configs!");
|
||||
return cameraConfigurations;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstractly match cameras
|
||||
*
|
||||
* @param detectedCamInfos Physical cameras unmatched and attached to the device
|
||||
* @param unloadedConfigs {@link CameraConfiguration}
|
||||
* @param checkUSBPath If we should compare the USB port/bus IDs
|
||||
* @param checkVidPid If we should compare USB VID and PID
|
||||
* @param checkBaseName If we should check {@link CameraInfo::getBaseName}
|
||||
* @param checkPath If we should check {@link CameraInfo::path} (eg /dev/videoN on Linux, or
|
||||
* usb#vid_05c8&pid_03df&mi_00#7&fa76035&0&0000#{e5323777-f976-4f5b-9b55-b94699c46e44}\global
|
||||
* on Windows). Note that path may change based on order cameras are plugged in/unplugged on
|
||||
* Linux, and should not be trusted to remain the same.
|
||||
* @return All matched or created new configs
|
||||
*/
|
||||
private List<CameraConfiguration> matchCamerasByStrategy(
|
||||
List<CameraInfo> detectedCamInfos,
|
||||
List<CameraConfiguration> unloadedConfigs,
|
||||
CameraMatchingOptions matchingOptions) {
|
||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
||||
List<CameraConfiguration> unloadedConfigsCopy =
|
||||
new ArrayList<CameraConfiguration>(unloadedConfigs);
|
||||
protected synchronized VisionSourceManagerState getVsmState() {
|
||||
var ret = new VisionSourceManagerState();
|
||||
|
||||
if (unloadedConfigsCopy.isEmpty()) return List.of();
|
||||
ret.allConnectedCameras = filterAllowedDevices(getConnectedCameras());
|
||||
ret.disabledConfigs =
|
||||
disabledCameraConfigs.values().stream().map(it -> it.toUiConfig()).toList();
|
||||
|
||||
logger.debug("Matching with options " + matchingOptions.toString());
|
||||
|
||||
for (CameraConfiguration config : unloadedConfigsCopy) {
|
||||
// Only run match path by id if the camera type is allowed. This allows us to specify matching
|
||||
// behavior per-camera-type
|
||||
if (matchingOptions.allowedTypes.contains(config.cameraType)) {
|
||||
logger.debug(
|
||||
String.format(
|
||||
"Trying to find a match for loaded camera %s (%s) with camera config: %s",
|
||||
config.baseName, config.uniqueName, config.toShortString()));
|
||||
|
||||
// Get matcher and filter against it, picking out the first match
|
||||
Predicate<CameraInfo> matches =
|
||||
getCameraMatcher(
|
||||
config,
|
||||
matchingOptions.checkUSBPath,
|
||||
matchingOptions.checkVidPid,
|
||||
matchingOptions.checkBaseName,
|
||||
matchingOptions.checkPath);
|
||||
var cameraInfo = detectedCamInfos.stream().filter(matches).findFirst().orElse(null);
|
||||
|
||||
// If we actually matched a camera to a config, remove that camera from the list
|
||||
// and add it to the output
|
||||
if (cameraInfo != null) {
|
||||
logger.debug(
|
||||
"Matched the config for "
|
||||
+ config.uniqueName
|
||||
+ " to the physical camera config above!");
|
||||
ret.add(mergeInfoIntoConfig(config, cameraInfo));
|
||||
detectedCamInfos.remove(cameraInfo);
|
||||
unloadedConfigs.remove(config);
|
||||
} else {
|
||||
logger.debug("No camera found for the config " + config.uniqueName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new {@link CameraConfiguration}s for unmatched cameras, and assign them a unique name
|
||||
* (unique in the set of (loaded configs, unloaded configs, loaded vision modules) at least)
|
||||
*/
|
||||
private List<CameraConfiguration> createConfigsForCameras(
|
||||
List<CameraInfo> detectedCameraList,
|
||||
List<CameraConfiguration> unloadedCamConfigs,
|
||||
List<CameraConfiguration> loadedConfigs) {
|
||||
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
|
||||
logger.debug(
|
||||
"After matching loaded configs, these cameras remained unmatched: "
|
||||
+ detectedCameraList.stream()
|
||||
.map(n -> String.valueOf(n))
|
||||
.collect(Collectors.joining("-", "{", "}")));
|
||||
for (CameraInfo info : detectedCameraList) {
|
||||
// create new camera config for all new cameras
|
||||
String baseName = info.getBaseName();
|
||||
String uniqueName = info.getHumanReadableName();
|
||||
|
||||
int suffix = 0;
|
||||
while (containsName(loadedConfigs, uniqueName)
|
||||
|| containsName(uniqueName)
|
||||
|| containsName(unloadedCamConfigs, uniqueName)
|
||||
|| containsName(ret, uniqueName)) {
|
||||
suffix++;
|
||||
uniqueName = String.format("%s (%d)", uniqueName, suffix);
|
||||
}
|
||||
|
||||
logger.info("Creating a new camera config for camera " + uniqueName);
|
||||
|
||||
String nickname = uniqueName;
|
||||
|
||||
CameraConfiguration configuration =
|
||||
new CameraConfiguration(baseName, uniqueName, nickname, info.path, info.otherPaths);
|
||||
|
||||
configuration.cameraType = info.cameraType;
|
||||
|
||||
ret.add(configuration);
|
||||
}
|
||||
return ret;
|
||||
protected void pushUiUpdate() {
|
||||
DataChangeService.getInstance()
|
||||
.publishEvent(OutgoingUIEvent.wrappedOf("visionSourceManager", getVsmState()));
|
||||
}
|
||||
|
||||
private CameraConfiguration mergeInfoIntoConfig(CameraConfiguration cfg, CameraInfo info) {
|
||||
if (!cfg.path.equals(info.path)) {
|
||||
logger.debug("Updating path config from " + cfg.path + " to " + info.path);
|
||||
cfg.path = info.path;
|
||||
}
|
||||
cfg.otherPaths = info.otherPaths;
|
||||
cfg.cameraType = info.cameraType;
|
||||
|
||||
if (cfg.otherPaths.length != info.otherPaths.length) {
|
||||
logger.debug(
|
||||
"Updating otherPath config from "
|
||||
+ Arrays.toString(cfg.otherPaths)
|
||||
+ " to "
|
||||
+ Arrays.toString(info.otherPaths));
|
||||
cfg.otherPaths = info.otherPaths.clone();
|
||||
} else {
|
||||
for (int i = 0; i < info.otherPaths.length; i++) {
|
||||
if (!cfg.otherPaths[i].equals(info.otherPaths[i])) {
|
||||
logger.debug(
|
||||
"Updating otherPath config from "
|
||||
+ Arrays.toString(cfg.otherPaths)
|
||||
+ " to "
|
||||
+ Arrays.toString(info.otherPaths));
|
||||
cfg.otherPaths = info.otherPaths.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
protected List<PVCameraInfo> getConnectedCameras() {
|
||||
List<PVCameraInfo> cameraInfos = new ArrayList<>();
|
||||
// find all connected cameras
|
||||
// cscore can return usb and csi cameras but csi are filtered out
|
||||
Stream.of(UsbCamera.enumerateUsbCameras())
|
||||
.map(c -> PVCameraInfo.fromUsbCameraInfo(c))
|
||||
.filter(c -> !(String.join("", c.otherPaths()).contains("csi-video")))
|
||||
.filter(c -> !c.name().equals("unicam"))
|
||||
.forEach(cameraInfos::add);
|
||||
if (LibCameraJNILoader.isSupported()) {
|
||||
// find all CSI cameras (Raspberry Pi cameras)
|
||||
Stream.of(LibCameraJNI.getCameraNames())
|
||||
.map(
|
||||
path -> {
|
||||
String name = LibCameraJNI.getSensorModel(path).getFriendlyName();
|
||||
return PVCameraInfo.fromCSICameraInfo(path, name);
|
||||
})
|
||||
.forEach(cameraInfos::add);
|
||||
}
|
||||
|
||||
return cfg;
|
||||
// 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);
|
||||
|
||||
return cameraInfos;
|
||||
}
|
||||
|
||||
public void setIgnoredCamerasRegex(String ignoredCamerasRegex) {
|
||||
this.ignoredCamerasRegex = ignoredCamerasRegex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out any blacklisted or ignored devices.
|
||||
*
|
||||
* @param allDevices
|
||||
* @return list of devices with blacklisted or ignore devices removed.
|
||||
*/
|
||||
private List<CameraInfo> filterAllowedDevices(List<CameraInfo> allDevices, Platform platform) {
|
||||
List<CameraInfo> filteredDevices = new ArrayList<>();
|
||||
private static List<PVCameraInfo> filterAllowedDevices(List<PVCameraInfo> allDevices) {
|
||||
Platform platform = Platform.getCurrentPlatform();
|
||||
ArrayList<PVCameraInfo> filteredDevices = new ArrayList<>();
|
||||
for (var device : allDevices) {
|
||||
if (deviceBlacklist.contains(device.name)) {
|
||||
boolean valid = false;
|
||||
if (deviceBlacklist.contains(device.name())) {
|
||||
logger.trace(
|
||||
"Skipping blacklisted device: \"" + device.name + "\" at \"" + device.path + "\"");
|
||||
} else if (device.name.matches(ignoredCamerasRegex)) {
|
||||
logger.trace("Skipping ignored device: \"" + device.name + "\" at \"" + device.path);
|
||||
} else if (device.getIsV4lCsiCamera()) {
|
||||
} else if (device.otherPaths.length == 0
|
||||
&& platform.osType == OSType.LINUX
|
||||
&& device.cameraType == CameraType.UsbCamera) {
|
||||
logger.trace(
|
||||
"Skipping device with no other paths: \"" + device.name + "\" at \"" + device.path);
|
||||
// If cscore hasnt passed this other paths aka a path by id or a path as in usb port then we
|
||||
// cant guarantee it is a valid camera.
|
||||
"Skipping blacklisted device: \"" + device.name() + "\" at \"" + device.path() + "\"");
|
||||
} else if (device instanceof PVCameraInfo.PVUsbCameraInfo usbDevice) {
|
||||
if (usbDevice.otherPaths.length == 0
|
||||
&& platform.osType == OSType.LINUX
|
||||
&& device.type() == CameraType.UsbCamera) {
|
||||
logger.trace(
|
||||
"Skipping device with no other paths: \""
|
||||
+ device.name()
|
||||
+ "\" at \""
|
||||
+ device.path());
|
||||
} else if (Arrays.stream(usbDevice.otherPaths).anyMatch(it -> it.contains("csi-video"))
|
||||
|| usbDevice.name().equals("unicam")) {
|
||||
logger.trace(
|
||||
"Skipping CSI device from CSCore: \""
|
||||
+ device.name()
|
||||
+ "\" at \""
|
||||
+ device.path()
|
||||
+ "\"");
|
||||
} else {
|
||||
valid = true;
|
||||
}
|
||||
} else {
|
||||
valid = true;
|
||||
}
|
||||
if (valid) {
|
||||
filteredDevices.add(device);
|
||||
logger.trace(
|
||||
"Adding local video device - \"" + device.name + "\" at \"" + device.path + "\"");
|
||||
"Adding local video device - \"" + device.name() + "\" at \"" + device.path() + "\"");
|
||||
}
|
||||
}
|
||||
return filteredDevices;
|
||||
}
|
||||
|
||||
private static List<VisionSource> loadVisionSourcesFromCamConfigs(
|
||||
List<CameraConfiguration> camConfigs, boolean createSources) {
|
||||
var cameraSources = new ArrayList<VisionSource>();
|
||||
for (var configuration : camConfigs) {
|
||||
// In unit tests, create dummy
|
||||
if (!createSources) {
|
||||
cameraSources.add(new TestSource(configuration));
|
||||
continue;
|
||||
}
|
||||
/**
|
||||
* Convert a configuration into a VisionSource. The VisionSource type is pulled from the {@link
|
||||
* CameraConfiguration}'s matchedCameraInfo. We depend on the underlying {@link VisionSource} to
|
||||
* be robust to disconnected sources at boot
|
||||
*
|
||||
* <p>Verify that nickname is unique within the set of desesrialized camera configurations, adding
|
||||
* random characters if this isn't the case
|
||||
*/
|
||||
protected VisionSource loadVisionSourceFromCamConfig(CameraConfiguration configuration) {
|
||||
logger.debug("Creating VisionSource for " + configuration.toShortString());
|
||||
|
||||
boolean is_pi = Platform.isRaspberryPi();
|
||||
|
||||
if (configuration.cameraType == CameraType.ZeroCopyPicam && is_pi) {
|
||||
// If the camera was loaded from libcamera then create its source using libcamera.
|
||||
var piCamSrc = new LibcameraGpuSource(configuration);
|
||||
cameraSources.add(piCamSrc);
|
||||
// First, make sure that nickname is globally unique since we use the nickname in NetworkTables.
|
||||
// "Just one more source of truth bro it'll real this time I promise"
|
||||
var currentNicknames = new ArrayList<String>();
|
||||
this.disabledCameraConfigs.values().stream()
|
||||
.map(it -> it.nickname)
|
||||
.forEach(currentNicknames::add);
|
||||
this.vmm.getModules().stream()
|
||||
.map(it -> it.getCameraConfiguration().nickname)
|
||||
.forEach(currentNicknames::add);
|
||||
// while it's a duplicate
|
||||
while (currentNicknames.contains(configuration.nickname)) {
|
||||
// if we already have a number, extract
|
||||
var pattern = Pattern.compile("(^.*) \\(([0-9]+)\\)$");
|
||||
var matcher = pattern.matcher(configuration.nickname);
|
||||
if (matcher.find()) {
|
||||
int oldNumber = Integer.parseInt(matcher.group(2));
|
||||
int newNumber = oldNumber + 1;
|
||||
configuration.nickname = matcher.group(1) + " (" + newNumber + ")";
|
||||
} else {
|
||||
var newCam = new USBCameraSource(configuration);
|
||||
if (!newCam.getCameraQuirks().hasQuirk(CameraQuirk.CompletelyBroken)
|
||||
&& !newCam.getSettables().videoModes.isEmpty()) {
|
||||
cameraSources.add(newCam);
|
||||
}
|
||||
configuration.nickname += " (1)";
|
||||
}
|
||||
logger.debug("Creating VisionSource for " + configuration.toShortString());
|
||||
}
|
||||
return cameraSources;
|
||||
|
||||
VisionSource source =
|
||||
switch (configuration.matchedCameraInfo.type()) {
|
||||
case UsbCamera -> new USBCameraSource(configuration);
|
||||
case ZeroCopyPicam -> new LibcameraGpuSource(configuration);
|
||||
case FileCamera -> new FileVisionSource(configuration);
|
||||
};
|
||||
|
||||
if (source.getFrameProvider() == null) {
|
||||
logger.error("Frame provider is null?");
|
||||
}
|
||||
if (source.getSettables() == null) {
|
||||
logger.error("Settables are null?");
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a given config list contains the given unique name.
|
||||
*
|
||||
* @param configList A list of camera configs.
|
||||
* @param uniqueName The unique name.
|
||||
* @return If the list of configs contains the unique name.
|
||||
*/
|
||||
private boolean containsName(
|
||||
final List<CameraConfiguration> configList, final String uniqueName) {
|
||||
return configList.stream()
|
||||
.anyMatch(configuration -> configuration.uniqueName.equals(uniqueName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current list of known cameras contains the given unique name.
|
||||
*
|
||||
* @param uniqueName The unique name.
|
||||
* @return If the list of cameras contains the unique name.
|
||||
*/
|
||||
private boolean containsName(final String uniqueName) {
|
||||
return VisionModuleManager.getInstance().getModules().stream()
|
||||
.anyMatch(camera -> camera.visionSource.cameraConfiguration.uniqueName.equals(uniqueName));
|
||||
public List<VisionModule> getVisionModules() {
|
||||
return vmm.getModules();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,22 +26,33 @@ import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.frame.FrameStaticProperties;
|
||||
|
||||
public abstract class VisionSourceSettables {
|
||||
protected static final Logger logger =
|
||||
new Logger(VisionSourceSettables.class, LogGroup.VisionModule);
|
||||
protected Logger logger;
|
||||
|
||||
private final CameraConfiguration configuration;
|
||||
|
||||
protected VisionSourceSettables(CameraConfiguration configuration) {
|
||||
this.configuration = configuration;
|
||||
this.logger =
|
||||
new Logger(VisionSourceSettables.class, configuration.nickname, LogGroup.VisionModule);
|
||||
}
|
||||
|
||||
protected FrameStaticProperties frameStaticProperties;
|
||||
protected HashMap<Integer, VideoMode> videoModes;
|
||||
protected FrameStaticProperties frameStaticProperties = null;
|
||||
protected HashMap<Integer, VideoMode> videoModes = new HashMap<>();
|
||||
|
||||
public CameraConfiguration getConfiguration() {
|
||||
return configuration;
|
||||
}
|
||||
|
||||
// If the device has been connected at least once, and we cached properties
|
||||
protected boolean cameraPropertiesCached = false;
|
||||
|
||||
/**
|
||||
* Runs exactly once the first time that the underlying device goes from disconnected to connected
|
||||
*/
|
||||
public void onCameraConnected() {
|
||||
cameraPropertiesCached = true;
|
||||
}
|
||||
|
||||
public abstract void setExposureRaw(double exposureRaw);
|
||||
|
||||
public abstract double getMinExposureRaw();
|
||||
@@ -109,7 +120,7 @@ public abstract class VisionSourceSettables {
|
||||
calculateFrameStaticProps();
|
||||
}
|
||||
|
||||
private void calculateFrameStaticProps() {
|
||||
protected void calculateFrameStaticProps() {
|
||||
var videoMode = getCurrentVideoMode();
|
||||
this.frameStaticProperties =
|
||||
new FrameStaticProperties(
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.photonvision.common.logging.LogLevel;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.common.util.file.JacksonUtils;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.CVPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.ColoredShapePipelineSettings;
|
||||
@@ -39,7 +40,8 @@ import org.photonvision.vision.target.TargetModel;
|
||||
public class ConfigTest {
|
||||
private static ConfigManager configMgr;
|
||||
private static final CameraConfiguration cameraConfig =
|
||||
new CameraConfiguration("TestCamera", "/dev/video420");
|
||||
new CameraConfiguration(
|
||||
"TestCamera", PVCameraInfo.fromFileInfo("TestCamera", "/dev/video420"));
|
||||
private static ReflectivePipelineSettings REFLECTIVE_PIPELINE_SETTINGS;
|
||||
private static ColoredShapePipelineSettings COLORED_SHAPE_PIPELINE_SETTINGS;
|
||||
private static AprilTagPipelineSettings APRIL_TAG_PIPELINE_SETTINGS;
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.photonvision.common.configuration;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -32,8 +33,7 @@ import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.camera.CameraType;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.ColoredShapePipelineSettings;
|
||||
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
|
||||
@@ -76,26 +76,18 @@ public class SQLConfigTest {
|
||||
|
||||
cfgLoader.load();
|
||||
|
||||
var testcamcfg =
|
||||
var testCamCfg =
|
||||
new CameraConfiguration(
|
||||
"basename",
|
||||
"a_unique_name",
|
||||
"a_nick_name",
|
||||
69,
|
||||
"a/path/idk",
|
||||
CameraType.UsbCamera,
|
||||
QuirkyCamera.getQuirkyCamera(-1, -1),
|
||||
List.of(),
|
||||
0,
|
||||
-1,
|
||||
-1);
|
||||
testcamcfg.pipelineSettings =
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
new UsbCameraInfo(0, "/dev/videoN", "some_name", new String[0], -1, 01)));
|
||||
|
||||
testCamCfg.pipelineSettings =
|
||||
List.of(
|
||||
new ReflectivePipelineSettings(),
|
||||
new AprilTagPipelineSettings(),
|
||||
new ColoredShapePipelineSettings());
|
||||
|
||||
cfgLoader.getConfig().addCameraConfig(testcamcfg);
|
||||
cfgLoader.getConfig().addCameraConfig(testCamCfg);
|
||||
cfgLoader.getConfig().getNetworkConfig().ntServerAddress = "5940";
|
||||
cfgLoader.saveToDisk();
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ public class MockUsbCameraSource extends USBCameraSource {
|
||||
public MockUsbCameraSource(CameraConfiguration config, int pid, int vid) {
|
||||
super(config);
|
||||
|
||||
getCameraConfiguration().cameraQuirks = QuirkyCamera.getQuirkyCamera(pid, vid, config.baseName);
|
||||
getCameraConfiguration().cameraQuirks =
|
||||
QuirkyCamera.getQuirkyCamera(pid, vid, config.matchedCameraInfo.name());
|
||||
|
||||
/** File used as frame provider */
|
||||
usbFrameProvider =
|
||||
|
||||
@@ -34,6 +34,7 @@ import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.dataflow.CVPipelineResultConsumer;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
import org.photonvision.vision.camera.QuirkyCamera;
|
||||
import org.photonvision.vision.camera.USBCameras.USBCameraSource;
|
||||
import org.photonvision.vision.frame.FrameProvider;
|
||||
@@ -90,6 +91,9 @@ public class VisionModuleManagerTest {
|
||||
public void remakeSettables() {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {}
|
||||
}
|
||||
|
||||
private static class TestSettables extends VisionSourceSettables {
|
||||
@@ -166,7 +170,9 @@ public class VisionModuleManagerTest {
|
||||
public void setupManager() {
|
||||
ConfigManager.getInstance().load();
|
||||
|
||||
var conf = new CameraConfiguration("Foo", "Bar");
|
||||
var vmm = new VisionModuleManager();
|
||||
|
||||
var conf = new CameraConfiguration(PVCameraInfo.fromFileInfo("Foo", "Bar"));
|
||||
var ffp =
|
||||
new FileFrameProvider(
|
||||
TestUtils.getWPIImagePath(TestUtils.WPI2019Image.kCargoStraightDark72in_HighRes, false),
|
||||
@@ -174,12 +180,12 @@ public class VisionModuleManagerTest {
|
||||
|
||||
var testSource = new TestSource(ffp, conf);
|
||||
|
||||
var modules = VisionModuleManager.getInstance().addSources(List.of(testSource));
|
||||
var module = vmm.addSource(testSource);
|
||||
var module0DataConsumer = new TestDataConsumer();
|
||||
|
||||
VisionModuleManager.getInstance().visionModules.get(0).addResultConsumer(module0DataConsumer);
|
||||
module.addResultConsumer(module0DataConsumer);
|
||||
|
||||
modules.forEach(VisionModule::start);
|
||||
module.start();
|
||||
|
||||
sleep(1500);
|
||||
|
||||
@@ -193,7 +199,7 @@ public class VisionModuleManagerTest {
|
||||
|
||||
var vmm = new VisionModuleManager();
|
||||
|
||||
var conf = new CameraConfiguration("Foo", "Bar");
|
||||
var conf = new CameraConfiguration(PVCameraInfo.fromFileInfo("Foo", "Bar"));
|
||||
conf.streamIndex = 1;
|
||||
var ffp =
|
||||
new FileFrameProvider(
|
||||
@@ -201,7 +207,7 @@ public class VisionModuleManagerTest {
|
||||
TestUtils.WPI2019Image.FOV);
|
||||
var testSource = new TestSource(ffp, conf);
|
||||
|
||||
var conf2 = new CameraConfiguration("Foo2", "Bar");
|
||||
var conf2 = new CameraConfiguration(PVCameraInfo.fromFileInfo("Foo2", "Bar2"));
|
||||
conf2.streamIndex = 0;
|
||||
var ffp2 =
|
||||
new FileFrameProvider(
|
||||
@@ -209,7 +215,7 @@ public class VisionModuleManagerTest {
|
||||
TestUtils.WPI2019Image.FOV);
|
||||
var testSource2 = new TestSource(ffp2, conf2);
|
||||
|
||||
var conf3 = new CameraConfiguration("Foo3", "Bar");
|
||||
var conf3 = new CameraConfiguration(PVCameraInfo.fromFileInfo("Foo3", "Bar3"));
|
||||
conf3.streamIndex = 0;
|
||||
var ffp3 =
|
||||
new FileFrameProvider(
|
||||
@@ -218,24 +224,23 @@ public class VisionModuleManagerTest {
|
||||
var testSource3 = new TestSource(ffp3, conf3);
|
||||
|
||||
// Arducam OV9281 UC844 raspberry pi test.
|
||||
var conf4 = new CameraConfiguration("Left", "dev/video1");
|
||||
var conf4 = new CameraConfiguration(PVCameraInfo.fromFileInfo("Left", "/dev/video1"));
|
||||
USBCameraSource usbSimulation = new MockUsbCameraSource(conf4, 0x6366, 0x0c45);
|
||||
|
||||
var conf5 = new CameraConfiguration("Right", "dev/video2");
|
||||
var conf5 = new CameraConfiguration(PVCameraInfo.fromFileInfo("Right", "/dev/video2"));
|
||||
USBCameraSource usbSimulation2 = new MockUsbCameraSource(conf5, 0x6366, 0x0c45);
|
||||
|
||||
var modules =
|
||||
vmm.addSources(
|
||||
List.of(testSource, testSource2, testSource3, usbSimulation, usbSimulation2));
|
||||
List.of(testSource, testSource2, testSource3, usbSimulation, usbSimulation2).stream()
|
||||
.map(vmm::addSource)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
System.out.println(
|
||||
Arrays.toString(
|
||||
modules.stream()
|
||||
.map(it -> it.visionSource.getCameraConfiguration().streamIndex)
|
||||
.toArray()));
|
||||
modules.stream().map(it -> it.getCameraConfiguration().streamIndex).toArray()));
|
||||
var idxs =
|
||||
modules.stream()
|
||||
.map(it -> it.visionSource.getCameraConfiguration().streamIndex)
|
||||
.map(it -> it.getCameraConfiguration().streamIndex)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertTrue(usbSimulation.equals(usbSimulation));
|
||||
|
||||
@@ -17,674 +17,223 @@
|
||||
|
||||
package org.photonvision.vision.processes;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import edu.wpi.first.cscore.UsbCameraInfo;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
import org.photonvision.common.hardware.Platform;
|
||||
import org.photonvision.common.logging.LogGroup;
|
||||
import org.photonvision.common.logging.LogLevel;
|
||||
import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.vision.camera.CameraInfo;
|
||||
import org.photonvision.vision.camera.CameraType;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.common.util.file.JacksonUtils;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
|
||||
public class VisionSourceManagerTest {
|
||||
@Test
|
||||
public void visionSourceTest() {
|
||||
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||
// Test harness that overrides getConnectedCameras, but uses USB cameras for
|
||||
// everything else
|
||||
// when we start testing libcamera stuff we'll need to mock more stuff out
|
||||
private static class TestVsm extends VisionSourceManager {
|
||||
public List<PVCameraInfo> testCameras = new ArrayList<>();
|
||||
|
||||
var inst = new VisionSourceManager();
|
||||
var cameraInfos = new ArrayList<CameraInfo>();
|
||||
ConfigManager.getInstance().clearConfig();
|
||||
@Override
|
||||
protected List<PVCameraInfo> getConnectedCameras() {
|
||||
return testCameras;
|
||||
}
|
||||
|
||||
public void teardown() {
|
||||
// release native resources
|
||||
var uniqueNames = getVisionModules().stream().map(VisionModule::uniqueName).toList();
|
||||
for (var name : uniqueNames) {
|
||||
deactivateVisionSource(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void loadLibraries() {
|
||||
TestUtils.loadLibraries();
|
||||
assertDoesNotThrow(PhotonTargetingJniLoader::load);
|
||||
assertTrue(PhotonTargetingJniLoader.isWorking);
|
||||
|
||||
// Broadcast all still calls into configmanager (ew) so set that up here
|
||||
ConfigManager.getInstance().load();
|
||||
}
|
||||
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
private TestVsm vsm = null;
|
||||
|
||||
var config3 =
|
||||
new CameraConfiguration(
|
||||
"thirdTestVideo",
|
||||
"thirdTestVideo",
|
||||
"thirdTestVideo",
|
||||
"dev/video1",
|
||||
new String[] {"by-id/123"});
|
||||
config3.usbVID = 3;
|
||||
config3.usbPID = 4;
|
||||
var config4 =
|
||||
new CameraConfiguration(
|
||||
"fourthTestVideo",
|
||||
"fourthTestVideo",
|
||||
"fourthTestVideo",
|
||||
"dev/video2",
|
||||
new String[] {"by-id/321"});
|
||||
config4.usbVID = 5;
|
||||
config4.usbPID = 6;
|
||||
@BeforeEach
|
||||
public void createVsm() {
|
||||
ConfigManager.getInstance().clearConfig();
|
||||
vsm = new TestVsm();
|
||||
}
|
||||
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(0, "dev/video0", "testVideo", new String[] {"/usb/path/0"}, 1, 2);
|
||||
|
||||
cameraInfos.add(info1);
|
||||
|
||||
inst.registerLoadedConfigs(config3, config4);
|
||||
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(inst.knownCameras.contains(info1));
|
||||
assertEquals(2, inst.unmatchedLoadedConfigs.size());
|
||||
|
||||
CameraInfo info2 =
|
||||
new CameraInfo(0, "dev/video1", "secondTestVideo", new String[] {"/usb/path/1"}, 2, 3);
|
||||
|
||||
cameraInfos.add(info2);
|
||||
|
||||
var cams = inst.matchCameras(cameraInfos, inst.unmatchedLoadedConfigs);
|
||||
|
||||
// assertEquals("testVideo (1)", cams.get(0).uniqueName); // Proper suffixing
|
||||
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(inst.knownCameras.contains(info2));
|
||||
assertEquals(2, inst.unmatchedLoadedConfigs.size());
|
||||
|
||||
CameraInfo info3 =
|
||||
new CameraInfo(0, "dev/video2", "thirdTestVideo", new String[] {"by-id/123"}, 3, 4);
|
||||
|
||||
CameraInfo info4 =
|
||||
new CameraInfo(0, "dev/video3", "fourthTestVideo", new String[] {"by-id/321"}, 5, 6);
|
||||
|
||||
cameraInfos.add(info4);
|
||||
|
||||
cams = inst.matchCameras(cameraInfos, inst.unmatchedLoadedConfigs);
|
||||
|
||||
var cam4 =
|
||||
cams.stream()
|
||||
.filter(
|
||||
cam -> cam.otherPaths.length > 0 && cam.otherPaths[0].equals(config4.otherPaths[0]))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
// If this is null, cam4 got matched to config3 instead of config4
|
||||
|
||||
assertEquals(cam4.nickname, config4.nickname);
|
||||
|
||||
cameraInfos.add(info3);
|
||||
|
||||
cams = inst.matchCameras(cameraInfos, inst.unmatchedLoadedConfigs);
|
||||
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(inst.knownCameras.contains(info2));
|
||||
assertTrue(inst.knownCameras.contains(info3));
|
||||
|
||||
var cam3 =
|
||||
cams.stream()
|
||||
.filter(
|
||||
cam -> cam.otherPaths.length > 0 && cam.otherPaths[0].equals(config3.otherPaths[0]))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
cam4 =
|
||||
cams.stream()
|
||||
.filter(
|
||||
cam -> cam.otherPaths.length > 0 && cam.otherPaths[0].equals(config4.otherPaths[0]))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
assertEquals(cam3.nickname, config3.nickname);
|
||||
assertEquals(cam4.nickname, config4.nickname);
|
||||
|
||||
CameraInfo info5 =
|
||||
new CameraInfo(
|
||||
2,
|
||||
"/dev/video2",
|
||||
"Left Camera",
|
||||
new String[] {
|
||||
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Left_Camera_12345-video-index0",
|
||||
"/dev/v4l/by-path/platform-xhci-hcd.0-usb-0:2:1.0-video-index0"
|
||||
},
|
||||
7,
|
||||
8);
|
||||
cameraInfos.add(info5);
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(inst.knownCameras.contains(info5));
|
||||
|
||||
CameraInfo info6 =
|
||||
new CameraInfo(
|
||||
3,
|
||||
"dev/video3",
|
||||
"Right Camera",
|
||||
new String[] {
|
||||
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Right_Camera_123456-video-index0",
|
||||
"/dev/v4l/by-path/platform-xhci-hcd.1-usb-0:1:1.0-video-index0"
|
||||
},
|
||||
9,
|
||||
10);
|
||||
cameraInfos.add(info6);
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(inst.knownCameras.contains(info6));
|
||||
|
||||
// RPI 5 CSI Tests
|
||||
|
||||
// CSI CAMERAS SHOULD NOT BE LOADED LIKE THIS THEY SHOULD GO THROUGH LIBCAM.
|
||||
CameraInfo info7 =
|
||||
new CameraInfo(
|
||||
4,
|
||||
"dev/video4",
|
||||
"CSICAM-DEV", // Typically rp1-cfe for unit test changed to CSICAM-DEV
|
||||
new String[] {"/dev/v4l/by-path/platform-1f00110000.csi-video-index0"},
|
||||
11,
|
||||
12);
|
||||
cameraInfos.add(info7);
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(!inst.knownCameras.contains(info7)); // This camera should not be recognized/used.
|
||||
|
||||
CameraInfo info8 =
|
||||
new CameraInfo(
|
||||
5,
|
||||
"dev/video8",
|
||||
"CSICAM-DEV", // Typically rp1-cfe for unit test changed to CSICAM-DEV
|
||||
new String[] {"/dev/v4l/by-path/platform-1f00110000.csi-video-index4"},
|
||||
13,
|
||||
14);
|
||||
cameraInfos.add(info8);
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(!inst.knownCameras.contains(info8)); // This camera should not be recognized/used.
|
||||
|
||||
CameraInfo info9 =
|
||||
new CameraInfo(
|
||||
6,
|
||||
"dev/video9",
|
||||
"CSICAM-DEV", // Typically rp1-cfe for unit test changed to CSICAM-DEV
|
||||
new String[] {"/dev/v4l/by-path/platform-1f00110000.csi-video-index5"},
|
||||
15,
|
||||
16);
|
||||
cameraInfos.add(info9);
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(!inst.knownCameras.contains(info9)); // This camera should not be recognized/used.
|
||||
assertEquals(6, inst.knownCameras.size());
|
||||
assertEquals(0, inst.unmatchedLoadedConfigs.size());
|
||||
|
||||
// RPI LIBCAMERA CSI CAMERA TESTS
|
||||
CameraInfo info10 =
|
||||
new CameraInfo(
|
||||
-1,
|
||||
"/base/soc/i2c0mux/i2c@0/ov9281@60",
|
||||
"OV9281", // Typically rp1-cfe for unit test changed to CSICAM-DEV
|
||||
new String[] {},
|
||||
-1,
|
||||
-1,
|
||||
CameraType.ZeroCopyPicam);
|
||||
cameraInfos.add(info10);
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(inst.knownCameras.contains(info10));
|
||||
assertEquals(7, inst.knownCameras.size());
|
||||
assertEquals(0, inst.unmatchedLoadedConfigs.size());
|
||||
|
||||
CameraInfo info11 =
|
||||
new CameraInfo(
|
||||
-1,
|
||||
"/base/soc/i2c0mux/i2c@1/ov9281@60",
|
||||
"OV9281", // Typically rp1-cfe for unit test changed to CSICAM-DEV
|
||||
new String[] {},
|
||||
-1,
|
||||
-1,
|
||||
CameraType.ZeroCopyPicam);
|
||||
cameraInfos.add(info11);
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(inst.knownCameras.contains(info11));
|
||||
assertEquals(8, inst.knownCameras.size());
|
||||
assertEquals(0, inst.unmatchedLoadedConfigs.size());
|
||||
|
||||
CameraInfo info12 =
|
||||
new CameraInfo(
|
||||
-1,
|
||||
" /base/axi/pcie@120000/rp1/i2c@80000/ov5647@36",
|
||||
"Camera Module v1",
|
||||
new String[] {},
|
||||
-1,
|
||||
-1,
|
||||
CameraType.ZeroCopyPicam);
|
||||
cameraInfos.add(info12);
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(inst.knownCameras.contains(info12));
|
||||
assertEquals(9, inst.knownCameras.size());
|
||||
assertEquals(0, inst.unmatchedLoadedConfigs.size());
|
||||
|
||||
CameraInfo info13 =
|
||||
new CameraInfo(
|
||||
-1,
|
||||
"/base/axi/pcie@120000/rp1/i2c@88000/imx708@1a",
|
||||
"Camera Module v3",
|
||||
new String[] {},
|
||||
-1,
|
||||
-1,
|
||||
CameraType.ZeroCopyPicam);
|
||||
cameraInfos.add(info13);
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
assertTrue(inst.knownCameras.contains(info13));
|
||||
assertEquals(10, inst.knownCameras.size());
|
||||
assertEquals(0, inst.unmatchedLoadedConfigs.size());
|
||||
@AfterEach
|
||||
public void teardownVsm() {
|
||||
vsm.teardown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisableInhibitPathChangeIdenticalCams() {
|
||||
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||
|
||||
var inst = new VisionSourceManager();
|
||||
ConfigManager.getInstance().clearConfig();
|
||||
ConfigManager.getInstance().load();
|
||||
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = false;
|
||||
|
||||
var CAM2_OLD_PATH =
|
||||
new String[] {"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"};
|
||||
var CAM2_NEW_PATH =
|
||||
new String[] {"/dev/v4l/by-path/platform-fc880080.usb-usb-0:1:1.3-video-index0"};
|
||||
|
||||
var CAM1_OLD_PATHS =
|
||||
new String[] {
|
||||
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
|
||||
};
|
||||
|
||||
var camera1_saved_config =
|
||||
new CameraConfiguration(
|
||||
"Arducam OV2311 USB Camera",
|
||||
"Arducam OV2311 USB Camera",
|
||||
"front-left",
|
||||
"/dev/video0",
|
||||
CAM1_OLD_PATHS);
|
||||
camera1_saved_config.usbVID = 3141;
|
||||
camera1_saved_config.usbPID = 25446;
|
||||
var camera2_saved_config =
|
||||
new CameraConfiguration(
|
||||
"Arducam OV2311 USB Camera",
|
||||
"Arducam OV2311 USB Camera (1)",
|
||||
"front-left",
|
||||
"/dev/video2",
|
||||
CAM2_OLD_PATH);
|
||||
camera2_saved_config.usbVID = 3141;
|
||||
camera2_saved_config.usbPID = 25446;
|
||||
|
||||
// And load our "old" configs
|
||||
inst.registerLoadedConfigs(camera1_saved_config, camera2_saved_config);
|
||||
|
||||
// Camera attached to new port, but strict matching disabled
|
||||
public void testCameraInfoSerde() throws InterruptedException, IOException {
|
||||
{
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(
|
||||
0, "/dev/video11", "Arducam OV2311 USB Camera", CAM1_OLD_PATHS, 3141, 25446);
|
||||
CameraInfo info2 =
|
||||
new CameraInfo(
|
||||
0, "/dev/video12", "Arducam OV2311 USB Camera", CAM2_NEW_PATH, 3141, 25446);
|
||||
var usb =
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
new UsbCameraInfo(
|
||||
2,
|
||||
"/dev/video2",
|
||||
"Left Camera", // renamed arducam
|
||||
new String[] {
|
||||
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Left_Camera_12345-video-index0",
|
||||
"/dev/v4l/by-path/platform-xhci-hcd.0-usb-0:2:1.0-video-index0"
|
||||
},
|
||||
7,
|
||||
8));
|
||||
|
||||
var cameraInfos = new ArrayList<CameraInfo>();
|
||||
cameraInfos.add(info1);
|
||||
cameraInfos.add(info2);
|
||||
List<VisionSource> ret1 = inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
// and check the new one got matched got matched
|
||||
assertEquals(2, ret1.size());
|
||||
assertEquals(
|
||||
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info1.path)).count());
|
||||
assertEquals(
|
||||
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info2.path)).count());
|
||||
var str = JacksonUtils.serializeToString(usb);
|
||||
System.out.println(str);
|
||||
System.out.println(JacksonUtils.deserialize(str, PVCameraInfo.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInhibitPathChangeIdenticalCams() {
|
||||
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||
|
||||
var inst = new VisionSourceManager();
|
||||
ConfigManager.getInstance().clearConfig();
|
||||
ConfigManager.getInstance().load();
|
||||
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = true;
|
||||
|
||||
var CAM2_OLD_PATH =
|
||||
new String[] {"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"};
|
||||
var CAM2_NEW_PATH =
|
||||
new String[] {"/dev/v4l/by-path/platform-fc880080.usb-usb-0:1:1.3-video-index0"};
|
||||
|
||||
var CAM1_OLD_PATHS =
|
||||
new String[] {
|
||||
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
|
||||
};
|
||||
|
||||
var camera1_saved_config =
|
||||
new CameraConfiguration(
|
||||
"Arducam OV2311 USB Camera",
|
||||
"Arducam OV2311 USB Camera (1)",
|
||||
"front-left",
|
||||
"/dev/video0",
|
||||
CAM1_OLD_PATHS);
|
||||
camera1_saved_config.usbVID = 3141;
|
||||
camera1_saved_config.usbPID = 25446;
|
||||
var camera2_saved_config =
|
||||
new CameraConfiguration(
|
||||
"Arducam OV2311 USB Camera",
|
||||
"Arducam OV2311 USB Camera (1)",
|
||||
"front-left",
|
||||
"/dev/video2",
|
||||
CAM2_OLD_PATH);
|
||||
camera2_saved_config.usbVID = 3141;
|
||||
camera2_saved_config.usbPID = 25446;
|
||||
|
||||
// And load our "old" configs
|
||||
inst.registerLoadedConfigs(camera1_saved_config, camera2_saved_config);
|
||||
|
||||
// initial pass with camera in the wrong spot
|
||||
{
|
||||
// Give our cameras new "paths" to fake the windows logic out. this should not
|
||||
// affect strict matching
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(
|
||||
0, "/dev/video11", "Arducam OV2311 USB Camera", CAM1_OLD_PATHS, 3141, 25446);
|
||||
CameraInfo info2 =
|
||||
new CameraInfo(
|
||||
0, "/dev/video12", "Arducam OV2311 USB Camera", CAM2_NEW_PATH, 3141, 25446);
|
||||
|
||||
var cameraInfos = new ArrayList<CameraInfo>();
|
||||
cameraInfos.add(info1);
|
||||
cameraInfos.add(info2);
|
||||
List<VisionSource> ret1 = inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
// Our cameras should be "known"
|
||||
assertTrue(inst.knownCameras.contains(info1));
|
||||
assertTrue(inst.knownCameras.contains(info2));
|
||||
assertEquals(2, inst.knownCameras.size());
|
||||
|
||||
// And we should have matched one camera
|
||||
assertEquals(1, ret1.size());
|
||||
// and only matched camera1, not 2
|
||||
assertEquals(
|
||||
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info1.path)).count());
|
||||
assertEquals(
|
||||
0, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info2.path)).count());
|
||||
}
|
||||
|
||||
// Now move our camera back
|
||||
{
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(
|
||||
0, "/dev/video11", "Arducam OV2311 USB Camera", CAM1_OLD_PATHS, 3141, 25446);
|
||||
CameraInfo info2 =
|
||||
new CameraInfo(
|
||||
0, "/dev/video12", "Arducam OV2311 USB Camera", CAM2_OLD_PATH, 3141, 25446);
|
||||
|
||||
var cameraInfos = new ArrayList<CameraInfo>();
|
||||
cameraInfos.add(info1);
|
||||
cameraInfos.add(info2);
|
||||
List<VisionSource> ret1 = inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
// and check the new one got matched got matched
|
||||
assertEquals(1, ret1.size());
|
||||
assertEquals(
|
||||
0, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info1.path)).count());
|
||||
assertEquals(
|
||||
1, ret1.stream().filter(it -> it.cameraConfiguration.path.equals(info2.path)).count());
|
||||
var csi =
|
||||
PVCameraInfo.fromCSICameraInfo(
|
||||
"/dev/v4l/by-path/platform-1f00110000.csi-video-index0", "rp1-cfe");
|
||||
var str = JacksonUtils.serializeToString(csi);
|
||||
System.out.println(str);
|
||||
System.out.println(JacksonUtils.deserialize(str, PVCameraInfo.class));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCSICameraMatching() {
|
||||
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||
public void testEmpty() {
|
||||
var vsm = new TestVsm();
|
||||
|
||||
// List of known cameras
|
||||
var cameraInfos = new ArrayList<CameraInfo>();
|
||||
List<CameraConfiguration> configs = List.of();
|
||||
vsm.registerLoadedConfigs(configs);
|
||||
|
||||
var inst = new VisionSourceManager();
|
||||
ConfigManager.getInstance().clearConfig();
|
||||
ConfigManager.getInstance().load();
|
||||
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = false;
|
||||
// And make assertions about the current matching state
|
||||
assertEquals(0, vsm.getVsmState().allConnectedCameras.size());
|
||||
assertEquals(0, vsm.getVsmState().disabledConfigs.size());
|
||||
assertEquals(0, vsm.vmm.getModules().size());
|
||||
}
|
||||
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(
|
||||
-1,
|
||||
"/base/soc/i2c0mux/i2c@0/ov9281@60",
|
||||
"OV9281", // Typically rp1-cfe for unit test changed to CSICAM-DEV
|
||||
new String[] {},
|
||||
-1,
|
||||
-1,
|
||||
CameraType.ZeroCopyPicam);
|
||||
@Test
|
||||
public void testFileVisionSource() throws InterruptedException, IOException {
|
||||
var fileCamera1 =
|
||||
PVCameraInfo.fromFileInfo(
|
||||
TestUtils.getApriltagImagePath(TestUtils.ApriltagTestImages.kTag1_640_480, false)
|
||||
.toAbsolutePath()
|
||||
.toString(),
|
||||
"kTag1_640_480");
|
||||
|
||||
CameraInfo info2 =
|
||||
new CameraInfo(
|
||||
-1,
|
||||
"/base/soc/i2c0mux/i2c@1/ov9281@60",
|
||||
"OV9281", // Typically rp1-cfe for unit test changed to CSICAM-DEV
|
||||
new String[] {},
|
||||
-1,
|
||||
-1,
|
||||
CameraType.ZeroCopyPicam);
|
||||
vsm.testCameras = List.of(fileCamera1);
|
||||
|
||||
var camera1_saved_config =
|
||||
List<CameraConfiguration> configs = List.of();
|
||||
vsm.registerLoadedConfigs(configs);
|
||||
|
||||
vsm.assignUnmatchedCamera(fileCamera1);
|
||||
|
||||
System.out.println(JacksonUtils.serializeToString(ConfigManager.getInstance().getConfig()));
|
||||
|
||||
// And make assertions about the current matching state
|
||||
assertEquals(1, vsm.getVsmState().allConnectedCameras.size());
|
||||
assertEquals(0, vsm.getVsmState().disabledConfigs.size());
|
||||
assertEquals(1, vsm.vmm.getModules().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnabledDisabled() throws InterruptedException {
|
||||
// GIVEN a VSM
|
||||
var vsm = new TestVsm();
|
||||
// AND one enabled camera, and one disabled camera
|
||||
var enabledCam =
|
||||
new CameraConfiguration(
|
||||
"OV9281", "OV9281", "test-1", "/base/soc/i2c0mux/i2c@0/ov9281@60", new String[0]);
|
||||
camera1_saved_config.cameraType = CameraType.ZeroCopyPicam;
|
||||
camera1_saved_config.usbVID = -1;
|
||||
camera1_saved_config.usbPID = -1;
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
new UsbCameraInfo(
|
||||
0,
|
||||
"/dev/video0",
|
||||
"Lifecam HD-3000",
|
||||
new String[] {"/dev/v4l/by-path/foobar1"},
|
||||
5940,
|
||||
5940)));
|
||||
enabledCam.deactivated = false;
|
||||
enabledCam.nickname = "Matt's awesome camera 1";
|
||||
|
||||
var camera2_saved_config =
|
||||
var disabledCam =
|
||||
new CameraConfiguration(
|
||||
"OV9281", "OV9281 (1)", "test-2", "/base/soc/i2c0mux/i2c@1/ov9281@60", new String[0]);
|
||||
camera2_saved_config.usbVID = -1;
|
||||
camera2_saved_config.usbPID = -1;
|
||||
camera2_saved_config.cameraType = CameraType.ZeroCopyPicam;
|
||||
PVCameraInfo.fromUsbCameraInfo(
|
||||
new UsbCameraInfo(
|
||||
1,
|
||||
"/dev/video1",
|
||||
"Lifecam HD-3000",
|
||||
new String[] {"/dev/v4l/by-path/foobar2"},
|
||||
5940,
|
||||
5940)));
|
||||
enabledCam.deactivated = true;
|
||||
enabledCam.nickname = "Matt's awesome camera 2";
|
||||
|
||||
cameraInfos.add(info1);
|
||||
cameraInfos.add(info2);
|
||||
vsm.testCameras = List.of(enabledCam.matchedCameraInfo, disabledCam.matchedCameraInfo);
|
||||
|
||||
// Try matching with both cameras being "known"
|
||||
inst.registerLoadedConfigs(camera1_saved_config, camera2_saved_config);
|
||||
var ret1 = inst.tryMatchCamImpl(cameraInfos);
|
||||
// WHEN cameras are loaded from disk
|
||||
vsm.registerLoadedConfigs(List.of(enabledCam, disabledCam));
|
||||
|
||||
// Our cameras should be "known"
|
||||
assertTrue(inst.knownCameras.contains(info1));
|
||||
assertTrue(inst.knownCameras.contains(info2));
|
||||
assertEquals(2, inst.knownCameras.size());
|
||||
assertEquals(2, ret1.size());
|
||||
// the enabled and disabled cameras will be matched
|
||||
assertEquals(2, vsm.getVsmState().allConnectedCameras.size());
|
||||
assertEquals(1, vsm.getVsmState().disabledConfigs.size());
|
||||
assertEquals(1, vsm.vmm.getModules().size());
|
||||
|
||||
// Exactly one camera should have the path we put in
|
||||
for (int i = 0; i < cameraInfos.size(); i++) {
|
||||
var testPath = cameraInfos.get(i).path;
|
||||
assertEquals(
|
||||
1, ret1.stream().filter(it -> testPath.equals(it.cameraConfiguration.path)).count());
|
||||
}
|
||||
Thread.sleep(2000);
|
||||
|
||||
vsm.teardown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoOtherPaths() {
|
||||
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||
public void testDuplicate() throws InterruptedException, IOException {
|
||||
var fileCamera1 =
|
||||
PVCameraInfo.fromFileInfo(
|
||||
TestUtils.getApriltagImagePath(TestUtils.ApriltagTestImages.kTag1_640_480, false)
|
||||
.toAbsolutePath()
|
||||
.toString(),
|
||||
"kTag1_640_480");
|
||||
CameraConfiguration camConf1 = new CameraConfiguration(fileCamera1);
|
||||
camConf1.deactivated = true;
|
||||
|
||||
// List of known cameras
|
||||
var cameraInfos = new ArrayList<CameraInfo>();
|
||||
var fileCamera2 =
|
||||
PVCameraInfo.fromFileInfo(
|
||||
TestUtils.getApriltagImagePath(TestUtils.ApriltagTestImages.kRobots, false)
|
||||
.toAbsolutePath()
|
||||
.toString(),
|
||||
"kTag1_640_480");
|
||||
CameraConfiguration camConf2 = new CameraConfiguration(fileCamera2);
|
||||
camConf2.nickname = camConf1.nickname + " (1)";
|
||||
camConf2.uniqueName += "owo";
|
||||
camConf2.deactivated = true;
|
||||
|
||||
var inst = new VisionSourceManager();
|
||||
ConfigManager.getInstance().clearConfig();
|
||||
ConfigManager.getInstance().load();
|
||||
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = false;
|
||||
var fileCamera3 =
|
||||
PVCameraInfo.fromFileInfo(
|
||||
TestUtils.getApriltagImagePath(TestUtils.ApriltagTestImages.kTag1_640_480, false)
|
||||
.toAbsolutePath()
|
||||
.toString(),
|
||||
"kTag1_640_480");
|
||||
|
||||
// Match empty camera infos
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
vsm.testCameras = List.of(fileCamera1, fileCamera2, fileCamera3);
|
||||
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(0, "/dev/video0", "Arducam OV2311 USB Camera", new String[] {}, 3141, 25446);
|
||||
List<CameraConfiguration> configs = List.of(camConf1, camConf2);
|
||||
vsm.registerLoadedConfigs(configs);
|
||||
|
||||
cameraInfos.add(info1);
|
||||
vsm.assignUnmatchedCamera(fileCamera3);
|
||||
|
||||
// Match two "new" cameras
|
||||
var ret1 = inst.tryMatchCamImpl(cameraInfos, Platform.LINUX_64);
|
||||
System.out.println(JacksonUtils.serializeToString(ConfigManager.getInstance().getConfig()));
|
||||
|
||||
// Our cameras should be "known"
|
||||
assertFalse(inst.knownCameras.contains(info1));
|
||||
assertEquals(0, inst.knownCameras.size());
|
||||
assertEquals(null, ret1);
|
||||
|
||||
// Match two "new" cameras
|
||||
var ret2 = inst.tryMatchCamImpl(cameraInfos, Platform.WINDOWS_64);
|
||||
|
||||
// Our cameras should be "known"
|
||||
assertTrue(inst.knownCameras.contains(info1));
|
||||
assertEquals(1, inst.knownCameras.size());
|
||||
assertEquals(1, ret2.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIdenticalCameras() {
|
||||
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
|
||||
|
||||
// List of known cameras
|
||||
var cameraInfos = new ArrayList<CameraInfo>();
|
||||
|
||||
var inst = new VisionSourceManager();
|
||||
ConfigManager.getInstance().clearConfig();
|
||||
ConfigManager.getInstance().load();
|
||||
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath = false;
|
||||
|
||||
// Match empty camera infos
|
||||
inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
CameraInfo info1 =
|
||||
new CameraInfo(
|
||||
0,
|
||||
"/dev/video0",
|
||||
"Arducam OV2311 USB Camera",
|
||||
new String[] {
|
||||
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0" // V4l doesnt assign
|
||||
// by-id paths that
|
||||
// are identical to
|
||||
// two different
|
||||
// cameras
|
||||
},
|
||||
3141,
|
||||
25446);
|
||||
CameraInfo info2 =
|
||||
new CameraInfo(
|
||||
0,
|
||||
"/dev/video2",
|
||||
"Arducam OV2311 USB Camera",
|
||||
new String[] {
|
||||
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||
"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"
|
||||
},
|
||||
3141,
|
||||
25446);
|
||||
|
||||
cameraInfos.add(info1);
|
||||
cameraInfos.add(info2);
|
||||
|
||||
// Match two "new" cameras
|
||||
var ret1 = inst.tryMatchCamImpl(cameraInfos);
|
||||
|
||||
// Our cameras should be "known"
|
||||
assertTrue(inst.knownCameras.contains(info1));
|
||||
assertTrue(inst.knownCameras.contains(info2));
|
||||
assertEquals(2, inst.knownCameras.size());
|
||||
assertEquals(2, ret1.size());
|
||||
|
||||
// Exactly one camera should have the path we put in
|
||||
for (int i = 0; i < cameraInfos.size(); i++) {
|
||||
var testPath = cameraInfos.get(i).getUSBPath().get();
|
||||
assertEquals(
|
||||
1,
|
||||
ret1.stream()
|
||||
.filter(it -> testPath.equals(it.cameraConfiguration.getUSBPath().get()))
|
||||
.count());
|
||||
}
|
||||
|
||||
// and the names should be unique
|
||||
for (int i = 0; i < ret1.size(); i++) {
|
||||
var thisName = ret1.get(i).cameraConfiguration.uniqueName;
|
||||
assertEquals(
|
||||
1,
|
||||
ret1.stream().filter(it -> thisName.equals(it.cameraConfiguration.uniqueName)).count());
|
||||
}
|
||||
|
||||
// duplicate cameras, same info, new ref
|
||||
var duplicateCameraInfos = new ArrayList<CameraInfo>();
|
||||
CameraInfo info1_dup =
|
||||
new CameraInfo(
|
||||
0,
|
||||
"/dev/video0",
|
||||
"Arducam OV2311 USB Camera",
|
||||
new String[] {
|
||||
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0" // V4l doesnt assign
|
||||
// by-id paths that
|
||||
// are identical to
|
||||
// two different
|
||||
// cameras
|
||||
},
|
||||
3141,
|
||||
25446);
|
||||
CameraInfo info2_dup =
|
||||
new CameraInfo(
|
||||
0,
|
||||
"/dev/video2",
|
||||
"Arducam OV2311 USB Camera",
|
||||
new String[] {
|
||||
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||
"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0"
|
||||
},
|
||||
3141,
|
||||
25446);
|
||||
|
||||
duplicateCameraInfos.add(info1_dup);
|
||||
duplicateCameraInfos.add(info2_dup);
|
||||
|
||||
inst.tryMatchCamImpl(duplicateCameraInfos);
|
||||
|
||||
// Our cameras should be "known", and we should only "know" two cameras still
|
||||
assertTrue(inst.knownCameras.contains(info1_dup));
|
||||
assertTrue(inst.knownCameras.contains(info2_dup));
|
||||
assertEquals(2, inst.knownCameras.size());
|
||||
|
||||
// duplicate cameras this simulates unplugging one and plugging the other in where v4l assigns
|
||||
// the same by-id path to the other camera
|
||||
var duplicateCameraInfos1 = new ArrayList<CameraInfo>();
|
||||
CameraInfo info3_dup =
|
||||
new CameraInfo(
|
||||
0,
|
||||
"/dev/video0",
|
||||
"Arducam OV2311 USB Camera",
|
||||
new String[] {
|
||||
"/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Arducam_OV2311_USB_Camera_UC621-video-index0",
|
||||
"/dev/v4l/by-path/platform-fc800000.usb-usb-0:1:1.0-video-index0"
|
||||
},
|
||||
3141,
|
||||
25446);
|
||||
CameraInfo info4_dup =
|
||||
new CameraInfo(
|
||||
0,
|
||||
"/dev/video2",
|
||||
"Arducam OV2311 USB Camera",
|
||||
new String[] {
|
||||
"/dev/v4l/by-path/platform-fc880000.usb-usb-0:1:1.0-video-index0" // V4l doesnt assign
|
||||
// by-id paths that
|
||||
// are identical to
|
||||
// two different
|
||||
// cameras
|
||||
},
|
||||
3141,
|
||||
25446);
|
||||
|
||||
duplicateCameraInfos1.add(info3_dup);
|
||||
duplicateCameraInfos1.add(info4_dup);
|
||||
|
||||
inst.tryMatchCamImpl(duplicateCameraInfos1);
|
||||
|
||||
// Our cameras should be "known", and we should only "know" two cameras still
|
||||
assertTrue(inst.knownCameras.contains(info3_dup));
|
||||
assertTrue(inst.knownCameras.contains(info4_dup));
|
||||
assertEquals(2, inst.knownCameras.size());
|
||||
// And make assertions about the current matching state
|
||||
assertEquals(3, vsm.getVsmState().allConnectedCameras.size());
|
||||
assertEquals(2, vsm.getVsmState().disabledConfigs.size());
|
||||
assertEquals(1, vsm.vmm.getModules().size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,9 @@ package org.photonvision;
|
||||
|
||||
import edu.wpi.first.hal.HAL;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.commons.cli.*;
|
||||
import org.photonvision.common.configuration.CameraConfiguration;
|
||||
import org.photonvision.common.configuration.ConfigManager;
|
||||
@@ -41,25 +38,17 @@ import org.photonvision.common.logging.Logger;
|
||||
import org.photonvision.common.logging.PvCSCoreLogger;
|
||||
import org.photonvision.common.networking.NetworkManager;
|
||||
import org.photonvision.common.util.TestUtils;
|
||||
import org.photonvision.common.util.numbers.IntegerCouple;
|
||||
import org.photonvision.jni.PhotonTargetingJniLoader;
|
||||
import org.photonvision.jni.RknnDetectorJNI;
|
||||
import org.photonvision.mrcal.MrCalJNILoader;
|
||||
import org.photonvision.raspi.LibCameraJNILoader;
|
||||
import org.photonvision.server.Server;
|
||||
import org.photonvision.vision.apriltag.AprilTagFamily;
|
||||
import org.photonvision.vision.camera.FileVisionSource;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
import org.photonvision.vision.opencv.CVMat;
|
||||
import org.photonvision.vision.opencv.ContourGroupingMode;
|
||||
import org.photonvision.vision.opencv.ContourShape;
|
||||
import org.photonvision.vision.pipeline.AprilTagPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.CVPipelineSettings;
|
||||
import org.photonvision.vision.pipeline.ColoredShapePipelineSettings;
|
||||
import org.photonvision.vision.pipeline.PipelineProfiler;
|
||||
import org.photonvision.vision.pipeline.ReflectivePipelineSettings;
|
||||
import org.photonvision.vision.processes.VisionModule;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
import org.photonvision.vision.processes.VisionSource;
|
||||
import org.photonvision.vision.processes.VisionSourceManager;
|
||||
import org.photonvision.vision.target.TargetModel;
|
||||
|
||||
@@ -126,11 +115,6 @@ public class Main {
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.hasOption("ignore-cameras")) {
|
||||
VisionSourceManager.getInstance()
|
||||
.setIgnoredCamerasRegex(cmd.getOptionValue("ignore-cameras"));
|
||||
}
|
||||
|
||||
if (cmd.hasOption("disable-networking")) {
|
||||
NetworkManager.getInstance().networkingIsDisabled = true;
|
||||
}
|
||||
@@ -146,156 +130,27 @@ public class Main {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void addTestModeFromFolder() {
|
||||
ConfigManager.getInstance().load();
|
||||
|
||||
try {
|
||||
List<VisionSource> collectedSources =
|
||||
Files.list(testModeFolder)
|
||||
.filter(p -> p.toFile().isFile())
|
||||
.map(
|
||||
p -> {
|
||||
try {
|
||||
CameraConfiguration camConf =
|
||||
new CameraConfiguration(
|
||||
p.getFileName().toString(), p.toAbsolutePath().toString());
|
||||
camConf.FOV = TestUtils.WPI2019Image.FOV; // Good guess?
|
||||
camConf.addCalibration(TestUtils.get2020LifeCamCoeffs(false));
|
||||
|
||||
var pipeSettings = new AprilTagPipelineSettings();
|
||||
pipeSettings.pipelineNickname = p.getFileName().toString();
|
||||
pipeSettings.outputShowMultipleTargets = true;
|
||||
pipeSettings.inputShouldShow = true;
|
||||
pipeSettings.outputShouldShow = false;
|
||||
pipeSettings.solvePNPEnabled = true;
|
||||
|
||||
var aprilTag = new AprilTagPipelineSettings();
|
||||
var psList = new ArrayList<CVPipelineSettings>();
|
||||
psList.add(aprilTag);
|
||||
camConf.pipelineSettings = psList;
|
||||
|
||||
return new FileVisionSource(camConf);
|
||||
} catch (Exception e) {
|
||||
logger.error("Couldn't load image " + p.getFileName().toString(), e);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ConfigManager.getInstance().unloadCameraConfigs();
|
||||
VisionModuleManager.getInstance().addSources(collectedSources).forEach(VisionModule::start);
|
||||
ConfigManager.getInstance().addCameraConfigurations(collectedSources);
|
||||
} catch (IOException e) {
|
||||
logger.error("Path does not exist!");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void addTestModeSources() {
|
||||
ConfigManager.getInstance().load();
|
||||
|
||||
var camConf2019 =
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2019");
|
||||
if (camConf2019 == null) {
|
||||
camConf2019 =
|
||||
new CameraConfiguration("WPI2019", TestUtils.getTestMode2019ImagePath().toString());
|
||||
camConf2019.FOV = TestUtils.WPI2019Image.FOV;
|
||||
camConf2019.calibrations.add(TestUtils.get2019LifeCamCoeffs(true));
|
||||
|
||||
var pipeline2019 = new ReflectivePipelineSettings();
|
||||
pipeline2019.pipelineNickname = "CargoShip";
|
||||
pipeline2019.targetModel = TargetModel.k2019DualTarget;
|
||||
pipeline2019.outputShowMultipleTargets = true;
|
||||
pipeline2019.contourGroupingMode = ContourGroupingMode.Dual;
|
||||
pipeline2019.inputShouldShow = true;
|
||||
|
||||
var psList2019 = new ArrayList<CVPipelineSettings>();
|
||||
psList2019.add(pipeline2019);
|
||||
camConf2019.pipelineSettings = psList2019;
|
||||
}
|
||||
|
||||
var camConf2020 =
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2020");
|
||||
if (camConf2020 == null) {
|
||||
camConf2020 =
|
||||
new CameraConfiguration("WPI2020", TestUtils.getTestMode2020ImagePath().toString());
|
||||
camConf2020.FOV = TestUtils.WPI2020Image.FOV;
|
||||
camConf2020.calibrations.add(TestUtils.get2019LifeCamCoeffs(true));
|
||||
|
||||
var pipeline2020 = new ReflectivePipelineSettings();
|
||||
pipeline2020.pipelineNickname = "OuterPort";
|
||||
pipeline2020.targetModel = TargetModel.k2020HighGoalOuter;
|
||||
camConf2020.calibrations.add(TestUtils.get2020LifeCamCoeffs(true));
|
||||
pipeline2020.inputShouldShow = true;
|
||||
|
||||
var psList2020 = new ArrayList<CVPipelineSettings>();
|
||||
psList2020.add(pipeline2020);
|
||||
camConf2020.pipelineSettings = psList2020;
|
||||
}
|
||||
|
||||
var camConf2022 =
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2022");
|
||||
if (camConf2022 == null) {
|
||||
camConf2022 =
|
||||
new CameraConfiguration("WPI2022", TestUtils.getTestMode2022ImagePath().toString());
|
||||
camConf2022.FOV = TestUtils.WPI2022Image.FOV;
|
||||
camConf2022.calibrations.add(TestUtils.get2019LifeCamCoeffs(true));
|
||||
|
||||
var pipeline2022 = new ReflectivePipelineSettings();
|
||||
pipeline2022.pipelineNickname = "OuterPort";
|
||||
pipeline2022.targetModel = TargetModel.k2020HighGoalOuter;
|
||||
pipeline2022.inputShouldShow = true;
|
||||
// camConf2020.calibrations.add(TestUtils.get2020LifeCamCoeffs(true));
|
||||
|
||||
var psList2022 = new ArrayList<CVPipelineSettings>();
|
||||
psList2022.add(pipeline2022);
|
||||
camConf2022.pipelineSettings = psList2022;
|
||||
}
|
||||
|
||||
CameraConfiguration camConf2023 =
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2023");
|
||||
if (camConf2023 == null) {
|
||||
camConf2023 =
|
||||
new CameraConfiguration(
|
||||
"WPI2023",
|
||||
TestUtils.getResourcesFolderPath(true)
|
||||
.resolve("testimages")
|
||||
.resolve(TestUtils.WPI2023Apriltags.k383_60_Angle2.path)
|
||||
.toString());
|
||||
|
||||
camConf2023.FOV = TestUtils.WPI2023Apriltags.FOV;
|
||||
camConf2023.calibrations.add(TestUtils.get2023LifeCamCoeffs(true));
|
||||
|
||||
var pipeline2023 = new AprilTagPipelineSettings();
|
||||
var path_split = Path.of(camConf2023.path).getFileName().toString();
|
||||
pipeline2023.pipelineNickname = path_split.replace(".png", "");
|
||||
pipeline2023.targetModel = TargetModel.kAprilTag6in_16h5;
|
||||
pipeline2023.inputShouldShow = true;
|
||||
pipeline2023.solvePNPEnabled = true;
|
||||
|
||||
var psList2023 = new ArrayList<CVPipelineSettings>();
|
||||
psList2023.add(pipeline2023);
|
||||
camConf2023.pipelineSettings = psList2023;
|
||||
}
|
||||
|
||||
CameraConfiguration camConf2024 =
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("WPI2024");
|
||||
if (camConf2024 == null) {
|
||||
if (camConf2024 == null || true) {
|
||||
camConf2024 =
|
||||
new CameraConfiguration(
|
||||
"WPI2024",
|
||||
TestUtils.getResourcesFolderPath(true)
|
||||
.resolve("testimages")
|
||||
.resolve(TestUtils.WPI2024Images.kSpeakerCenter_143in.path)
|
||||
.toString());
|
||||
PVCameraInfo.fromFileInfo(
|
||||
TestUtils.getResourcesFolderPath(true)
|
||||
.resolve("testimages")
|
||||
.resolve(TestUtils.WPI2024Images.kSpeakerCenter_143in.path)
|
||||
.toString(),
|
||||
"WPI2024"));
|
||||
|
||||
camConf2024.FOV = TestUtils.WPI2024Images.FOV;
|
||||
// same camera as 2023
|
||||
camConf2024.calibrations.add(TestUtils.get2023LifeCamCoeffs(true));
|
||||
|
||||
var pipeline2024 = new AprilTagPipelineSettings();
|
||||
var path_split = Path.of(camConf2024.path).getFileName().toString();
|
||||
var path_split = Path.of(camConf2024.matchedCameraInfo.path()).getFileName().toString();
|
||||
pipeline2024.pipelineNickname = path_split.replace(".jpg", "");
|
||||
pipeline2024.targetModel = TargetModel.kAprilTag6p5in_36h11;
|
||||
pipeline2024.tagFamily = AprilTagFamily.kTag36h11;
|
||||
@@ -307,47 +162,11 @@ public class Main {
|
||||
camConf2024.pipelineSettings = psList2024;
|
||||
}
|
||||
|
||||
// Colored shape testing
|
||||
var camConfShape =
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations().get("Shape");
|
||||
|
||||
// If we haven't saved shape settings, create a new one
|
||||
if (camConfShape == null) {
|
||||
camConfShape =
|
||||
new CameraConfiguration(
|
||||
"Shape",
|
||||
TestUtils.getPowercellImagePath(TestUtils.PowercellTestImages.kPowercell_test_1, true)
|
||||
.toString());
|
||||
var settings = new ColoredShapePipelineSettings();
|
||||
settings.hsvHue = new IntegerCouple(0, 35);
|
||||
settings.hsvSaturation = new IntegerCouple(82, 255);
|
||||
settings.hsvValue = new IntegerCouple(62, 255);
|
||||
settings.contourShape = ContourShape.Triangle;
|
||||
settings.outputShowMultipleTargets = true;
|
||||
settings.circleAccuracy = 15;
|
||||
settings.inputShouldShow = true;
|
||||
camConfShape.addPipelineSetting(settings);
|
||||
}
|
||||
|
||||
var collectedSources = new ArrayList<VisionSource>();
|
||||
|
||||
var fvsShape = new FileVisionSource(camConfShape);
|
||||
var fvs2019 = new FileVisionSource(camConf2019);
|
||||
var fvs2020 = new FileVisionSource(camConf2020);
|
||||
var fvs2022 = new FileVisionSource(camConf2022);
|
||||
var fvs2023 = new FileVisionSource(camConf2023);
|
||||
var fvs2024 = new FileVisionSource(camConf2024);
|
||||
|
||||
collectedSources.add(fvs2024);
|
||||
// collectedSources.add(fvs2023);
|
||||
// collectedSources.add(fvs2022);
|
||||
// collectedSources.add(fvsShape);
|
||||
// collectedSources.add(fvs2020);
|
||||
// collectedSources.add(fvs2019);
|
||||
var cameraConfigs = List.of(camConf2024);
|
||||
|
||||
ConfigManager.getInstance().unloadCameraConfigs();
|
||||
VisionModuleManager.getInstance().addSources(collectedSources).forEach(VisionModule::start);
|
||||
ConfigManager.getInstance().addCameraConfigurations(collectedSources);
|
||||
cameraConfigs.stream().forEach(ConfigManager.getInstance()::addCameraConfiguration);
|
||||
VisionSourceManager.getInstance().registerLoadedConfigs(cameraConfigs);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
@@ -475,21 +294,21 @@ public class Main {
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
// todo - should test mode just add test mode sources, but still allow local usb cameras to be
|
||||
// added?
|
||||
if (!isTestMode) {
|
||||
logger.debug("Loading VisionSourceManager...");
|
||||
VisionSourceManager.getInstance()
|
||||
.registerLoadedConfigs(
|
||||
ConfigManager.getInstance().getConfig().getCameraConfigurations().values());
|
||||
|
||||
VisionSourceManager.getInstance().registerTimedTask();
|
||||
} else {
|
||||
if (testModeFolder == null) {
|
||||
addTestModeSources();
|
||||
} else {
|
||||
addTestModeFromFolder();
|
||||
}
|
||||
}
|
||||
|
||||
VisionSourceManager.getInstance().registerTimedTasks();
|
||||
|
||||
logger.info("Starting server...");
|
||||
HardwareManager.getInstance().setRunning(true);
|
||||
Server.initialize(DEFAULT_WEBPORT);
|
||||
|
||||
@@ -52,7 +52,8 @@ import org.photonvision.common.util.file.JacksonUtils;
|
||||
import org.photonvision.common.util.file.ProgramDirectoryUtilities;
|
||||
import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
|
||||
import org.photonvision.vision.camera.CameraQuirk;
|
||||
import org.photonvision.vision.processes.VisionModuleManager;
|
||||
import org.photonvision.vision.camera.PVCameraInfo;
|
||||
import org.photonvision.vision.processes.VisionSourceManager;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
public class RequestHandler {
|
||||
@@ -395,7 +396,7 @@ public class RequestHandler {
|
||||
logger.info("Changing camera FOV to: " + fov);
|
||||
logger.info("Changing quirks to: " + settings.quirksToChange.toString());
|
||||
|
||||
var module = VisionModuleManager.getInstance().getModule(index);
|
||||
var module = VisionSourceManager.getInstance().vmm.getModule(index);
|
||||
module.setFov(fov);
|
||||
module.changeCameraQuirks(settings.quirksToChange);
|
||||
|
||||
@@ -474,7 +475,7 @@ public class RequestHandler {
|
||||
try {
|
||||
index = kObjectMapper.readTree(ctx.bodyInputStream()).get("index").asInt();
|
||||
|
||||
var calData = VisionModuleManager.getInstance().getModule(index).endCalibration();
|
||||
var calData = VisionSourceManager.getInstance().vmm.getModule(index).endCalibration();
|
||||
if (calData == null) {
|
||||
ctx.result("The calibration process failed");
|
||||
ctx.status(500);
|
||||
@@ -551,7 +552,7 @@ public class RequestHandler {
|
||||
String name = data.get("name").asText();
|
||||
int idx = data.get("cameraIndex").asInt();
|
||||
|
||||
VisionModuleManager.getInstance().getModule(idx).setCameraNickname(name);
|
||||
VisionSourceManager.getInstance().vmm.getModule(idx).setCameraNickname(name);
|
||||
ctx.status(200);
|
||||
ctx.result("Successfully changed the camera name to: " + name);
|
||||
logger.info("Successfully changed the camera name to: " + name);
|
||||
@@ -581,7 +582,8 @@ public class RequestHandler {
|
||||
var observationIdx = Integer.parseInt(ctx.queryParam("snapshotIdx"));
|
||||
|
||||
CameraCalibrationCoefficients calList =
|
||||
VisionModuleManager.getInstance()
|
||||
VisionSourceManager.getInstance()
|
||||
.vmm
|
||||
.getModule(idx)
|
||||
.getStateAsCameraConfig()
|
||||
.calibrations
|
||||
@@ -631,7 +633,7 @@ public class RequestHandler {
|
||||
var width = Integer.parseInt(ctx.queryParam("width"));
|
||||
var height = Integer.parseInt(ctx.queryParam("height"));
|
||||
|
||||
var cc = VisionModuleManager.getInstance().getModule(idx).getStateAsCameraConfig();
|
||||
var cc = VisionSourceManager.getInstance().vmm.getModule(idx).getStateAsCameraConfig();
|
||||
|
||||
CameraCalibrationCoefficients calList =
|
||||
cc.calibrations.stream()
|
||||
@@ -818,19 +820,63 @@ public class RequestHandler {
|
||||
FileUtils.deleteDirectory(cameraDir);
|
||||
}
|
||||
|
||||
// prevent -anyone- else from writing camera configs -- but flush first
|
||||
ConfigManager.getInstance().saveToDisk();
|
||||
ConfigManager.getInstance().setWriteTaskEnabled(false);
|
||||
ConfigManager.getInstance().disableFlushOnShutdown();
|
||||
// remove the config from the global config and force-flush
|
||||
ConfigManager.getInstance().getConfig().removeCameraConfig(name);
|
||||
ConfigManager.getInstance().saveToDisk();
|
||||
VisionSourceManager.getInstance().deleteVisionSource(name);
|
||||
|
||||
ctx.status(200);
|
||||
restartProgram();
|
||||
} catch (IOException e) {
|
||||
// todo
|
||||
logger.error("asdf", e);
|
||||
ctx.status(500);
|
||||
}
|
||||
}
|
||||
|
||||
public static void onActivateMatchedCameraRequest(Context ctx) {
|
||||
logger.info(ctx.queryString().toString());
|
||||
|
||||
String uniqueName = ctx.queryParam("uniqueName");
|
||||
|
||||
if (VisionSourceManager.getInstance().reactivateDisabledCameraConfig(uniqueName)) {
|
||||
ctx.status(200);
|
||||
} else {
|
||||
ctx.status(403);
|
||||
}
|
||||
|
||||
ctx.result("Successfully assigned camera with unique name: " + uniqueName);
|
||||
}
|
||||
|
||||
public static void onAssignUnmatchedCameraRequest(Context ctx) {
|
||||
logger.info(ctx.queryString().toString());
|
||||
|
||||
PVCameraInfo camera;
|
||||
try {
|
||||
camera = JacksonUtils.deserialize(ctx.queryParam("cameraInfo"), PVCameraInfo.class);
|
||||
} catch (IOException e) {
|
||||
ctx.status(401);
|
||||
return;
|
||||
}
|
||||
|
||||
if (VisionSourceManager.getInstance().assignUnmatchedCamera(camera)) {
|
||||
ctx.status(200);
|
||||
} else {
|
||||
ctx.status(403);
|
||||
}
|
||||
|
||||
ctx.result("Successfully assigned camera: " + camera);
|
||||
}
|
||||
|
||||
public static void onUnassignCameraRequest(Context ctx) {
|
||||
logger.info(ctx.queryString().toString());
|
||||
|
||||
String uniqueName = ctx.queryParam("uniqueName");
|
||||
|
||||
if (VisionSourceManager.getInstance().deactivateVisionSource(uniqueName)) {
|
||||
ctx.status(200);
|
||||
} else {
|
||||
ctx.status(403);
|
||||
}
|
||||
|
||||
ctx.status(200);
|
||||
|
||||
ctx.result("Successfully assigned camera with unique name: " + uniqueName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +136,9 @@ public class Server {
|
||||
app.get("/api/utils/getCalibrationJSON", RequestHandler::onCalibrationExportRequest);
|
||||
app.post("/api/utils/nukeConfigDirectory", RequestHandler::onNukeConfigDirectory);
|
||||
app.post("/api/utils/nukeOneCamera", RequestHandler::onNukeOneCamera);
|
||||
app.post("/api/utils/activateMatchedCamera", RequestHandler::onActivateMatchedCameraRequest);
|
||||
app.post("/api/utils/assignUnmatchedCamera", RequestHandler::onAssignUnmatchedCameraRequest);
|
||||
app.post("/api/utils/unassignCamera", RequestHandler::onUnassignCameraRequest);
|
||||
|
||||
// Calibration
|
||||
app.post("/api/calibration/end", RequestHandler::onCalibrationEndRequest);
|
||||
|
||||
Reference in New Issue
Block a user