Convert to user selected camera matching (#1556)

This commit is contained in:
oh-yes-0-fps
2025-01-01 03:04:20 -05:00
committed by GitHub
parent b2e70a7257
commit 418eada0b5
67 changed files with 2710 additions and 1948 deletions

View File

@@ -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)
![](https://www.plantuml.com/plantuml/png/VP5FQnin4CNl-XI3JotK-DAJAI6fIw6GfOMbFkKoramSqTKVfF6MVFkETfKsei6trVpUldbwkYs2MIv-CeI29omCcn5d9XXPn8LpsG0MAErWaggTTGc3m6P05nRizQD7HrTS3336IxOC0mOySrwqS_5lIeT8bubxgVTNN9jRhpYCXvXNP8lLpokxsWvZNcwtlQaNsSDzH8B773sGAxzC7MvlDFSUxeXWKie4DeP7futelC8z73AZCDnPSJD35xKOh5F5DR31IU3d-1aiUive06PTlSRTm_V4eH4uFJ-4Aamn2xmxFMyJojDx0x2AjtNn-WSJ73_UltRyzC_o2mjRQH1IZecpE4t5WPOmX_5R7sPof_NyVvwghNbK-LVL1sbErTneFLqxNxF27pdEZZXNs8gjbJFrhHdYLxMredrx1Obm70QZvnUBtKxdJE2NnosxNVj3qIYO1GB_Rb3DEZAlQxKPowMuS7u8oIMUNE0F84-PaOgvvK0NF_q1)
## 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.
![](https://www.plantuml.com/plantuml/svg/POvDJyCm343l-HLMxnFt7j14uJ099AHkSCvSCopoCSLE-FjaxQW8kpbwpy_PYjgasJk3qJb2vHW4kZrxcc1lvGjURB0dIXrO0LLlpBakCFBP1eNkZQLkm1XpGchS8hvLXt68YMQ6WdLiyJCVqNfATZRSxwkLtka8XzriP3P6rM_kww4U7hac2oK8z0qJ5KOIKwJYvLOFJo5VUafm61zWYOjPwEPQ6M88X4fJuyoPzKD_IyEuMwrLk8rLhOrbxk4rooVWwbmvE1Rz9rbKBdJ7OHakInzy4hEbC6NlVW00)
![](images/matching_ui.png)
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
![](https://www.plantuml.com/plantuml/svg/VL7B3jCm4BpxArOzWKIK2wSALOKWf4gDG8fQBhquzggrY1_u4TI_PvCufGRKK-ATySpiU1yYzp7fWJdwAg4SDn4stx67qs43F41I9NHMGLa3dKrU8BJSy2lwcJa6_LzgQsKQ_g9g_K8rgvMCfckiNo0H1FsMy57rWclqV6OCw-b5e1o4iQIg7MNVmaSfeRz3CkfdGZ0am6YUmOuR5UyWRYX-X7M-XSOZZmX5_i2uY6ga-RG5uqE4K_S9SYAWORLRTjZ2LuSc8-HzCHFHMH_XJN-l78-tjmpomjNakDn02UVtnrKHZPnDckvGcZng-DU7kBCFCH-imk1PdDRzy2VoPumeuYhcl7L87UDKIj795q-CRzwEIgAVmDpaqNA9igoCINpgBDUhyvj42-UsPNHU9UgQvgIXvvSCTRtUe7UAt4Sm-2k395OWus9BiGM6eCprOfnoE2Y3xo3UF78Ps1wDJ7hu3G00)
### 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.
![](https://www.plantuml.com/plantuml/svg/ROxBJiCm44Nt_efHLtIH7yWYgWWB8gYeI0eRDXDdW97yABQd5N-FvV1G8bOUppdNrxkOC2InHftooPfFw19idcc4OxS1Z22yH4ySsJlelGHDi4U7RnIAUOxsNtNl9p4hrQxKjczzeC9qr7bSudiUDLeAM0ppSrDAk6foRmqtX3hn6HD16GXcvSMDdo2EFuJ0vOtATexO77aawxDdo_TKNbLLCvVNq1eV_vwuwbxXs5zllwNV_Xe6mZ3vYrkeRTzjvvv6k8Q3n7TmT86OC541LG6tmt20Xpkr8pU9DLy0)
### 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.
![](https://www.plantuml.com/plantuml/svg/VP9BYnD158NtzIikirAmoSPL8s5YH1n8SB1duYQRsrtNcGlrAElHadzlLVgXXP9LACvNvvmwwViGqSUabN3vbmTsQ2BSVQSUdX_k00CahgKJ1xO6EflyG714Wo_ah-GOz7_HevL9KOrgVSDrTgk9VRUtVfA6C5XFjNpWVa1D7g-4Maut2ir5X4ZSR7Ft5huH3f57Z0II0_QA94msPzDV81d-cGWCQX82LOJdxYCuwoEmWHH8G9cWsIPkuSlJqoFyG5R9ao0ZXIXIZcbXxwaax4eKGVNm8DO2OrWpvWvN-sOxFRw5huxCh41_EPkrp9l-qZYChsy5m0GtKt2vGH9Exm-BOobMGlRTGnsoxlTlJc5BJYPNgWgOuUNL7_vK_aIHXhYOEMyT-SWKCbLDyzbduj7RaINv8ix_py6Y95bF9YJzjTcyiixmJag85ax7eyZdnMApsSdYeQ-VGDXibXijT15z14E_5b6CbJ9EiRdsG26mUJaRnuuK6te7yTKJoY3koSYarMy0)
# 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

View File

@@ -4,4 +4,5 @@
:maxdepth: 1
image-rotation
time-sync
camera-matching
```

View File

@@ -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",

View File

@@ -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": {

View File

@@ -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 });

View File

@@ -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;

View File

@@ -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

View File

@@ -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>

View File

@@ -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 -->

View File

@@ -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%"
/>

View 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>

View 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>

View File

@@ -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"
/>

View File

@@ -14,7 +14,8 @@ const props = withDefaults(
}>(),
{
disabled: false,
labelCols: 2
labelCols: 2,
switchCols: 8
}
);

View File

@@ -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"
/>

View File

@@ -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) }}&nbsp;FPS &ndash;</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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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",

View File

@@ -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,

View File

@@ -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];
},
/**

View File

@@ -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", {

View File

@@ -67,6 +67,7 @@ export interface MultitagResult {
}
export interface PipelineResult {
sequenceID: number;
fps: number;
latency: number;
targets: PhotonTarget[];

View File

@@ -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 {

View File

@@ -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 {

View 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>
&nbsp;<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>

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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());

View File

@@ -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()));
}

View File

@@ -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)
+ "]";
}
}

View File

@@ -19,6 +19,6 @@ package org.photonvision.vision.camera;
public enum CameraType {
UsbCamera,
HttpCamera,
ZeroCopyPicam
ZeroCopyPicam,
FileCamera // special case for File-based vision sources
}

View File

@@ -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<>();

View File

@@ -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);
}
}

View File

@@ -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'");
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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));

View File

@@ -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;
}
}

View File

@@ -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) */

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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
+ "]";
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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()));
}
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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();
}
}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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();

View File

@@ -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 =

View File

@@ -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));

View File

@@ -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());
}
}

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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);