Compare commits

...

15 Commits

Author SHA1 Message Date
Matt
ec66645667 Update build.yml (#1249) 2024-02-20 16:28:50 -05:00
Vasista Vovveti
39aaa34520 update wpilib to 2024.3.1 (#1246) 2024-02-20 15:08:52 -05:00
Vasista Vovveti
4a3200d0c0 Run apt update and install sqlite3 (#1247) 2024-02-19 21:37:55 -05:00
Matt
01dc7ea5ce Properly check camera info equality and handle zero cameras (#1245)
- Fix CameraInfo equality check (which prevents the same camera on a new usb port from being enumerated by us)
- Fix warning prints
- Make matchCamerasOnlyByPath apply to Windows
- Add unit tests
2024-02-19 12:34:57 -05:00
Matt
2a9502be3d Add matching by base-name only (fused off by only by path) (#1238) 2024-02-18 21:00:14 -05:00
amquake
39216db143 [photonlib] Invert simulated target yaw (#1243) 2024-02-18 20:59:54 -05:00
Matt
428f926ac2 Actually properly match cameras by name fr this time (#1237)
Our current code matches cameras in this order (which I think is objectively wrong and stupid)

- by-id (/dev/v4l/by-id/product-string)
- by path (/dev/videoN)
- product string/name, but ascii only
- asks cscore to reconnect to cameras using `path`, which on linux is actually /dev/videoN. This isn't guaranteed to stick to a camera if you replug them weirdly at runtime.

This is silly and does not consider the actual physical usb port. I propose instead, in this order:

- By physical usb port path and base name
- by physical usb port path and USB VID/PID
- By base name only (with a toggle switch to disable this, and create a new VisionModule instead)
- Give cscore /dev/video/by-path on Linux systems, pinning Photon USBCameras to a particular usb port once created.

This changes lots of things so stay paranoid!
2024-02-16 16:05:47 -05:00
Matt
4efeb3d412 Load libwinpthread-1.dll before libgcc_s_seh-1 (#1228) 2024-02-16 16:05:16 -05:00
Matt
6a2d83e19b Upload docs to VPS via SFTP (#1235)
Still in testing, might break our docs for now
2024-02-12 19:57:23 -05:00
Matt
1c0d92641f Check empty mean errors in calibration card (#1229)
Fixes calibration card disappearing if calibdb calibration was used
2024-02-12 15:55:31 -05:00
DeltaDizzy
9653c46bdb fix cpp and java photoncamera names (#1230) 2024-02-11 04:27:25 -05:00
Chris Gerth
3738e7821b fix latency calculation (#1227) 2024-02-09 18:45:38 -06:00
Tim Winters
0eb0a4e3c5 Store the last pose on update (#1207)
* Store the last pose on update

* Don't clear lastPose if pose isn't calculated

---------

Co-authored-by: Mohammad Durrani <46766905+mdurrani808@users.noreply.github.com>
2024-02-05 09:50:36 -05:00
Chris Gerth
7666f152bb Fix chessboard gen for unique square sizes (#1217) 2024-02-05 09:48:39 -05:00
Craig Schardt
45a39f6609 Remove duplicate video modes (#1221)
(Fixes #1219)
2024-02-04 22:42:01 -05:00
38 changed files with 810 additions and 191 deletions

View File

@@ -290,13 +290,13 @@ jobs:
- os: ubuntu-latest - os: ubuntu-latest
artifact-name: LinuxArm64 artifact-name: LinuxArm64
image_suffix: orangepi5 image_suffix: orangepi5
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.6/photonvision_opi5.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.9/photonvision_opi5.img.xz
cpu: cortex-a8 cpu: cortex-a8
image_additional_mb: 4096 image_additional_mb: 4096
- os: ubuntu-latest - os: ubuntu-latest
artifact-name: LinuxArm64 artifact-name: LinuxArm64
image_suffix: orangepi5plus image_suffix: orangepi5plus
image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.6/photonvision_opi5plus.img.xz image_url: https://github.com/PhotonVision/photon-image-modifier/releases/download/v2024.0.9/photonvision_opi5plus.img.xz
cpu: cortex-a8 cpu: cortex-a8
image_additional_mb: 4096 image_additional_mb: 4096

View File

@@ -68,10 +68,6 @@ jobs:
release: release:
needs: [build-client, run_docs] needs: [build-client, run_docs]
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
@@ -79,14 +75,12 @@ jobs:
- uses: actions/download-artifact@v4 - uses: actions/download-artifact@v4
- run: find . - run: find .
- name: copy file via ssh password
- name: Setup Pages uses: appleboy/scp-action@v0.1.7
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with: with:
# Upload entire repository host: ${{ secrets.WEBMASTER_SSH_HOST }}
path: '.' username: ${{ secrets.WEBMASTER_SSH_USERNAME }}
- name: Deploy to GitHub Pages password: ${{ secrets.WEBMASTER_SSH_KEY }}
id: deployment port: ${{ secrets.WEBMASTER_SSH_PORT }}
uses: actions/deploy-pages@v4 source: "*"
target: /var/www/html/photonvision-docs/

View File

@@ -4,7 +4,7 @@ plugins {
id "com.diffplug.spotless" version "6.24.0" id "com.diffplug.spotless" version "6.24.0"
id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false id "edu.wpi.first.NativeUtils" version "2024.6.1" apply false
id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2" id "edu.wpi.first.wpilib.repositories.WPILibRepositoriesPlugin" version "2020.2"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id 'edu.wpi.first.WpilibTools' version '1.3.0' id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.4' apply false id 'com.google.protobuf' version '0.9.4' apply false
} }
@@ -24,7 +24,7 @@ allprojects {
apply from: "versioningHelper.gradle" apply from: "versioningHelper.gradle"
ext { ext {
wpilibVersion = "2024.2.1" wpilibVersion = "2024.3.1"
wpimathVersion = wpilibVersion wpimathVersion = wpilibVersion
openCVversion = "4.8.0-2" openCVversion = "4.8.0-2"
joglVersion = "2.4.0-rc-20200307" joglVersion = "2.4.0-rc-20200307"

View File

@@ -26,7 +26,9 @@ const getUniqueVideoFormatsByResolution = (): VideoFormat[] => {
const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution); const calib = useCameraSettingsStore().getCalibrationCoeffs(format.resolution);
if (calib !== undefined) { if (calib !== undefined) {
// For each error, square it, sum the squares, and divide by total points N // For each error, square it, sum the squares, and divide by total points N
format.mean = calib.meanErrors.reduce((a, b) => a + b) / calib.meanErrors.length; if (calib.meanErrors.length)
format.mean = calib.meanErrors.reduce((a, b) => a + b, 0) / calib.meanErrors.length;
else format.mean = NaN;
format.horizontalFOV = format.horizontalFOV =
2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI); 2 * Math.atan2(format.resolution.width / 2, calib.cameraIntrinsics.data[0]) * (180 / Math.PI);
@@ -102,7 +104,7 @@ const downloadCalibBoard = () => {
const yPos = chessboardStartY + squareY * squareSizeIn.value; const yPos = chessboardStartY + squareY * squareSizeIn.value;
// Only draw the odd squares to create the chessboard pattern // Only draw the odd squares to create the chessboard pattern
if ((xPos + yPos + 0.25) % 2 === 0) { if (squareY % 2 != squareX % 2) {
doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F"); doc.rect(xPos, yPos, squareSizeIn.value, squareSizeIn.value, "F");
} }
} }
@@ -256,7 +258,7 @@ const setSelectedVideoFormat = (format: VideoFormat) => {
> >
<td>{{ getResolutionString(value.resolution) }}</td> <td>{{ getResolutionString(value.resolution) }}</td>
<td> <td>
{{ value.mean !== undefined ? (isNaN(value.mean) ? "NaN" : value.mean.toFixed(2) + "px") : "-" }} {{ value.mean !== undefined ? (isNaN(value.mean) ? "Unknown" : value.mean.toFixed(2) + "px") : "-" }}
</td> </td>
<td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td> <td>{{ value.horizontalFOV !== undefined ? value.horizontalFOV.toFixed(2) + "°" : "-" }}</td>
<td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td> <td>{{ value.verticalFOV !== undefined ? value.verticalFOV.toFixed(2) + "°" : "-" }}</td>

View File

@@ -59,7 +59,8 @@ const settingsHaveChanged = (): boolean => {
a.shouldPublishProto !== b.shouldPublishProto || a.shouldPublishProto !== b.shouldPublishProto ||
a.networkManagerIface !== b.networkManagerIface || a.networkManagerIface !== b.networkManagerIface ||
a.setStaticCommand !== b.setStaticCommand || a.setStaticCommand !== b.setStaticCommand ||
a.setDHCPcommand !== b.setDHCPcommand a.setDHCPcommand !== b.setDHCPcommand ||
a.matchCamerasOnlyByPath !== b.matchCamerasOnlyByPath
); );
}; };
@@ -77,6 +78,7 @@ const saveGeneralSettings = () => {
setStaticCommand: tempSettingsStruct.value.setStaticCommand || "", setStaticCommand: tempSettingsStruct.value.setStaticCommand || "",
shouldManage: tempSettingsStruct.value.shouldManage, shouldManage: tempSettingsStruct.value.shouldManage,
shouldPublishProto: tempSettingsStruct.value.shouldPublishProto, shouldPublishProto: tempSettingsStruct.value.shouldPublishProto,
matchCamerasOnlyByPath: tempSettingsStruct.value.matchCamerasOnlyByPath,
staticIp: tempSettingsStruct.value.staticIp staticIp: tempSettingsStruct.value.staticIp
}; };
@@ -137,6 +139,8 @@ watchEffect(() => {
<template> <template>
<v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492"> <v-card dark class="mb-3 pr-6 pb-3" style="background-color: #006492">
<v-card-title>Global Settings</v-card-title>
<v-divider />
<v-card-title>Networking</v-card-title> <v-card-title>Networking</v-card-title>
<div class="ml-5"> <div class="ml-5">
<v-form ref="form" v-model="settingsValid"> <v-form ref="form" v-model="settingsValid">
@@ -254,6 +258,9 @@ watchEffect(() => {
> >
This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work! This mode is intended for debugging; it should be off for proper usage. PhotonLib will NOT work!
</v-banner> </v-banner>
<v-divider />
<v-card-title>Miscellaneous</v-card-title>
<pv-switch <pv-switch
v-model="tempSettingsStruct.shouldPublishProto" v-model="tempSettingsStruct.shouldPublishProto"
label="Also Publish Protobuf" label="Also Publish Protobuf"
@@ -272,6 +279,32 @@ watchEffect(() => {
This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using This mode is intended for debugging; it should be off for field use. You may notice a performance hit by using
this mode. this mode.
</v-banner> </v-banner>
<pv-switch
v-model="tempSettingsStruct.matchCamerasOnlyByPath"
label="Strictly match ONLY known cameras"
tooltip="ONLY match cameras by the USB port they're plugged into + (basename or USB VID/PID), and never only by the device product string. Also disables automatic detection of new cameras."
class="mt-3 mb-2"
:label-cols="4"
/>
<v-banner
v-show="tempSettingsStruct.matchCamerasOnlyByPath"
rounded
color="red"
class="mb-3"
text-color="white"
icon="mdi-information-outline"
>
Physical cameras will be strictly matched to camera configurations using physical USB port they are plugged
into, in addition to device name and other USB metadata. Additionally, no new cameras are allowed to be added.
This setting is useful for guaranteeing that an already known and configured camera can never be matched as an
"unknown"/"new" camera, which resets pipelines and calibration data.
<p />
Cameras will NOT be matched if they change USB ports, and new cameras plugged into this coprocessor will NOT
be automatically recognized or configured for vision processing.
<p />
To add a new camera to this coprocessor, disable this setting, connect the camera, and re-enable.
</v-banner>
<v-divider class="mb-3" />
</v-form> </v-form>
<v-btn <v-btn
color="accent" color="accent"

View File

@@ -44,7 +44,9 @@ export const useSettingsStore = defineStore("settings", {
connName: "Example Wired Connection", connName: "Example Wired Connection",
devName: "eth0" devName: "eth0"
} }
] ],
networkingDisabled: false,
matchCamerasOnlyByPath: false
}, },
lighting: { lighting: {
supported: true, supported: true,

View File

@@ -47,6 +47,7 @@ export interface NetworkSettings {
setDHCPcommand?: string; setDHCPcommand?: string;
networkInterfaceNames: NetworkInterfaceType[]; networkInterfaceNames: NetworkInterfaceType[];
networkingDisabled: boolean; networkingDisabled: boolean;
matchCamerasOnlyByPath: boolean;
} }
export type ConfigurableNetworkSettings = Omit< export type ConfigurableNetworkSettings = Omit<

View File

@@ -23,6 +23,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.LogGroup;
import org.photonvision.common.logging.Logger; import org.photonvision.common.logging.Logger;
import org.photonvision.vision.calibration.CameraCalibrationCoefficients; import org.photonvision.vision.calibration.CameraCalibrationCoefficients;
@@ -51,6 +52,12 @@ public class CameraConfiguration {
@JsonIgnore public String[] otherPaths = {}; @JsonIgnore public String[] otherPaths = {};
@JsonProperty("usbVID")
public int usbVID = -1;
@JsonProperty("usbPID")
public int usbPID = -1;
public CameraType cameraType = CameraType.UsbCamera; public CameraType cameraType = CameraType.UsbCamera;
public double FOV = 70; public double FOV = 70;
public final List<CameraCalibrationCoefficients> calibrations; public final List<CameraCalibrationCoefficients> calibrations;
@@ -98,7 +105,9 @@ public class CameraConfiguration {
@JsonProperty("cameraType") CameraType cameraType, @JsonProperty("cameraType") CameraType cameraType,
@JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks, @JsonProperty("cameraQuirks") QuirkyCamera cameraQuirks,
@JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations, @JsonProperty("calibration") List<CameraCalibrationCoefficients> calibrations,
@JsonProperty("currentPipelineIndex") int currentPipelineIndex) { @JsonProperty("currentPipelineIndex") int currentPipelineIndex,
@JsonProperty("usbVID") int usbVID,
@JsonProperty("usbPID") int usbPID) {
this.baseName = baseName; this.baseName = baseName;
this.uniqueName = uniqueName; this.uniqueName = uniqueName;
this.nickname = nickname; this.nickname = nickname;
@@ -108,6 +117,8 @@ public class CameraConfiguration {
this.cameraQuirks = cameraQuirks; this.cameraQuirks = cameraQuirks;
this.calibrations = calibrations != null ? calibrations : new ArrayList<>(); this.calibrations = calibrations != null ? calibrations : new ArrayList<>();
this.currentPipelineIndex = currentPipelineIndex; this.currentPipelineIndex = currentPipelineIndex;
this.usbPID = usbPID;
this.usbVID = usbVID;
logger.debug( logger.debug(
"Creating camera configuration for " "Creating camera configuration for "
@@ -156,6 +167,17 @@ public class CameraConfiguration {
calibrations.add(calibration); calibrations.add(calibration);
} }
/**
* 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
*/
@JsonIgnore
public Optional<String> getUSBPath() {
return Arrays.stream(otherPaths).filter(path -> path.contains("/by-path/")).findFirst();
}
@Override @Override
public String toString() { public String toString() {
return "CameraConfiguration [baseName=" return "CameraConfiguration [baseName="

View File

@@ -20,6 +20,24 @@ package org.photonvision.common.configuration;
public class HardwareSettings { public class HardwareSettings {
public int ledBrightnessPercentage = 100; public int ledBrightnessPercentage = 100;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ledBrightnessPercentage;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
HardwareSettings other = (HardwareSettings) obj;
if (ledBrightnessPercentage != other.ledBrightnessPercentage) return false;
return true;
}
@Override @Override
public String toString() { public String toString() {
return "HardwareSettings [ledBrightnessPercentage=" + ledBrightnessPercentage + "]"; return "HardwareSettings [ledBrightnessPercentage=" + ledBrightnessPercentage + "]";

View File

@@ -39,6 +39,14 @@ public class NetworkConfig {
public boolean shouldManage; public boolean shouldManage;
public boolean shouldPublishProto = false; 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_IFACE_STRING = "${interface}";
@JsonIgnore public static final String NM_IP_STRING = "${ipaddr}"; @JsonIgnore public static final String NM_IP_STRING = "${ipaddr}";
@@ -76,7 +84,8 @@ public class NetworkConfig {
@JsonProperty("shouldPublishProto") boolean shouldPublishProto, @JsonProperty("shouldPublishProto") boolean shouldPublishProto,
@JsonProperty("networkManagerIface") String networkManagerIface, @JsonProperty("networkManagerIface") String networkManagerIface,
@JsonProperty("setStaticCommand") String setStaticCommand, @JsonProperty("setStaticCommand") String setStaticCommand,
@JsonProperty("setDHCPcommand") String setDHCPcommand) { @JsonProperty("setDHCPcommand") String setDHCPcommand,
@JsonProperty("matchCamerasOnlyByPath") boolean matchCamerasOnlyByPath) {
this.ntServerAddress = ntServerAddress; this.ntServerAddress = ntServerAddress;
this.connectionType = connectionType; this.connectionType = connectionType;
this.staticIp = staticIp; this.staticIp = staticIp;
@@ -86,6 +95,7 @@ public class NetworkConfig {
this.networkManagerIface = networkManagerIface; this.networkManagerIface = networkManagerIface;
this.setStaticCommand = setStaticCommand; this.setStaticCommand = setStaticCommand;
this.setDHCPcommand = setDHCPcommand; this.setDHCPcommand = setDHCPcommand;
this.matchCamerasOnlyByPath = matchCamerasOnlyByPath;
setShouldManage(shouldManage); setShouldManage(shouldManage);
} }

View File

@@ -53,6 +53,7 @@ public class MrCalJNILoader extends PhotonJNICommon {
"libcolamd", "libcolamd",
"libccolamd", "libccolamd",
"openblas", "openblas",
"libwinpthread-1",
"libgcc_s_seh-1", "libgcc_s_seh-1",
"libquadmath-0", "libquadmath-0",
"libgfortran-5", "libgfortran-5",

View File

@@ -19,6 +19,8 @@ package org.photonvision.vision.camera;
import edu.wpi.first.cscore.UsbCameraInfo; import edu.wpi.first.cscore.UsbCameraInfo;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional;
import org.photonvision.common.hardware.Platform;
public class CameraInfo extends UsbCameraInfo { public class CameraInfo extends UsbCameraInfo {
public final CameraType cameraType; public final CameraType cameraType;
@@ -68,15 +70,54 @@ public class CameraInfo extends UsbCameraInfo {
return getBaseName().replaceAll(" ", "_"); 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 @Override
public boolean equals(Object o) { public boolean equals(Object obj) {
if (o == this) return true; if (this == obj) return true;
if (!(o instanceof UsbCameraInfo || o instanceof CameraInfo)) return false; if (obj == null) return false;
UsbCameraInfo other = (UsbCameraInfo) o; if (getClass() != obj.getClass()) return false;
return path.equals(other.path) CameraInfo other = (CameraInfo) obj;
// && a.dev == b.dev (dev is not constant in Windows)
&& name.equals(other.name) // Windows device number is not significant. See
&& productId == other.productId // https://github.com/wpilibsuite/allwpilib/blob/4b94a64b06057c723d6fcafeb1a45f55a70d179a/cscore/src/main/native/windows/UsbCameraImpl.cpp#L1128
&& vendorId == other.vendorId; 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.otherPaths).containsAll(Arrays.asList(other.otherPaths))) 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

@@ -0,0 +1,93 @@
/*
* 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 java.util.*;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.vision.frame.Frame;
import org.photonvision.vision.frame.FrameProvider;
import org.photonvision.vision.frame.FrameThresholdType;
import org.photonvision.vision.opencv.ImageRotationMode;
import org.photonvision.vision.pipe.impl.HSVPipe.HSVParams;
import org.photonvision.vision.processes.VisionSource;
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);
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera(config.usbVID, config.usbVID, config.baseName);
}
@Override
public FrameProvider getFrameProvider() {
return new FrameProvider() {
@Override
public Frame get() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'get'");
}
@Override
public String getName() {
return cameraConfiguration.uniqueName;
}
@Override
public void requestFrameThresholdType(FrameThresholdType type) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestFrameThresholdType'");
}
@Override
public void requestFrameRotation(ImageRotationMode rotationMode) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestFrameRotation'");
}
@Override
public void requestFrameCopies(boolean copyInput, boolean copyOutput) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestFrameCopies'");
}
@Override
public void requestHsvSettings(HSVParams params) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'requestHsvSettings'");
}
};
}
@Override
public VisionSourceSettables getSettables() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'getSettables'");
}
@Override
public boolean isVendorCamera() {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'isVendorCamera'");
}
}

View File

@@ -49,9 +49,17 @@ public class USBCameraSource extends VisionSource {
super(config); super(config);
logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera); logger = new Logger(USBCameraSource.class, config.nickname, LogGroup.Camera);
camera = new UsbCamera(config.nickname, config.path); // 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); 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 (getCameraConfiguration().cameraQuirks == null) if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks = getCameraConfiguration().cameraQuirks =
QuirkyCamera.getQuirkyCamera( QuirkyCamera.getQuirkyCamera(
@@ -395,6 +403,7 @@ public class USBCameraSource extends VisionSource {
// Sort by resolution // Sort by resolution
var sortedList = var sortedList =
videoModesList.stream() videoModesList.stream()
.distinct() // remove redundant video mode entries
.sorted(((a, b) -> (b.width + b.height) - (a.width + a.height))) .sorted(((a, b) -> (b.width + b.height) - (a.width + a.height)))
.collect(Collectors.toList()); .collect(Collectors.toList());
Collections.reverse(sortedList); Collections.reverse(sortedList);

View File

@@ -23,6 +23,7 @@ import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.photonvision.common.configuration.CameraConfiguration; import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager; import org.photonvision.common.configuration.ConfigManager;
@@ -38,6 +39,7 @@ import org.photonvision.vision.camera.CameraInfo;
import org.photonvision.vision.camera.CameraQuirk; import org.photonvision.vision.camera.CameraQuirk;
import org.photonvision.vision.camera.CameraType; import org.photonvision.vision.camera.CameraType;
import org.photonvision.vision.camera.LibcameraGpuSource; import org.photonvision.vision.camera.LibcameraGpuSource;
import org.photonvision.vision.camera.TestSource;
import org.photonvision.vision.camera.USBCameraSource; import org.photonvision.vision.camera.USBCameraSource;
public class VisionSourceManager { public class VisionSourceManager {
@@ -145,8 +147,8 @@ public class VisionSourceManager {
} }
// Return no new sources because there are no new sources // Return no new sources because there are no new sources
if (connectedCameras.isEmpty() && !cameraInfos.isEmpty()) { if (connectedCameras.isEmpty()) {
if (hasWarnedNoCameras) { if (!hasWarnedNoCameras) {
logger.warn( logger.warn(
"No cameras were detected! Check that all cameras are connected, and that the path is correct."); "No cameras were detected! Check that all cameras are connected, and that the path is correct.");
hasWarnedNoCameras = true; hasWarnedNoCameras = true;
@@ -164,7 +166,7 @@ public class VisionSourceManager {
// Debug prints // Debug prints
for (var info : connectedCameras) { for (var info : connectedCameras) {
logger.info("Adding local video device - \"" + info.name + "\" at \"" + info.path + "\""); logger.info("Detected unmatched physical camera: " + info.toString());
} }
if (!unmatchedLoadedConfigs.isEmpty()) if (!unmatchedLoadedConfigs.isEmpty())
@@ -185,7 +187,7 @@ public class VisionSourceManager {
"Unloaded configs: " "Unloaded configs: "
+ unmatchedLoadedConfigs.stream() + unmatchedLoadedConfigs.stream()
.map(it -> it.nickname) .map(it -> it.nickname)
.collect(Collectors.joining())); .collect(Collectors.joining(", ")));
hasWarned = true; hasWarned = true;
} }
@@ -194,13 +196,8 @@ public class VisionSourceManager {
if (matchedCameras.isEmpty()) return null; if (matchedCameras.isEmpty()) return null;
// for unit tests only!
if (!createSources) {
return List.of();
}
// Turn these camera configs into vision sources // Turn these camera configs into vision sources
var sources = loadVisionSourcesFromCamConfigs(matchedCameras); var sources = loadVisionSourcesFromCamConfigs(matchedCameras, createSources);
// Print info about each vision source // Print info about each vision source
for (var src : sources) { for (var src : sources) {
@@ -216,6 +213,52 @@ public class VisionSourceManager {
return sources; return sources;
} }
/**
* Get a predicate for checking cameras against a saved config.
*
* @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)
*/
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: "
+ camCfgToString(savedConfig));
}
return (CameraInfo physicalCamera) -> {
var matches = true;
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));
}
return matches;
};
}
/** /**
* Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on * Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on
* disk. * disk.
@@ -226,35 +269,133 @@ public class VisionSourceManager {
*/ */
public List<CameraConfiguration> matchCameras( public List<CameraConfiguration> matchCameras(
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> loadedCamConfigs) { List<CameraInfo> detectedCamInfos, List<CameraConfiguration> loadedCamConfigs) {
return matchCameras(
detectedCamInfos,
loadedCamConfigs,
ConfigManager.getInstance().getConfig().getNetworkConfig().matchCamerasOnlyByPath);
}
private static final String camCfgToString(CameraConfiguration c) {
return new StringBuilder()
.append("[baseName=")
.append(c.baseName)
.append(", uniqueName=")
.append(c.uniqueName)
.append(", otherPaths=")
.append(Arrays.toString(c.otherPaths))
.append(", vid=")
.append(c.usbVID)
.append(", pid=")
.append(c.usbPID)
.append("]")
.toString();
}
/**
* 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); var detectedCameraList = new ArrayList<>(detectedCamInfos);
ArrayList<CameraConfiguration> cameraConfigurations = new ArrayList<CameraConfiguration>(); ArrayList<CameraConfiguration> cameraConfigurations = new ArrayList<CameraConfiguration>();
ArrayList<CameraConfiguration> unloadedConfigs = ArrayList<CameraConfiguration> unloadedConfigs =
new ArrayList<CameraConfiguration>(loadedCamConfigs); new ArrayList<CameraConfiguration>(loadedCamConfigs);
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
cameraConfigurations.addAll(matchByPathByID(detectedCameraList, unloadedConfigs)); logger.info("Matching by usb port & name & USB VID/PID...");
else logger.debug("Skipping matchByPath no configs or cameras left to match");
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
cameraConfigurations.addAll(matchByPath(detectedCameraList, unloadedConfigs));
else logger.debug("Skipping matchByPath no configs or cameras left to match");
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0)
cameraConfigurations.addAll(matchByName(detectedCameraList, unloadedConfigs));
else logger.debug("Skipping matchByName no configs or cameras left to match");
if (detectedCameraList.size() > 0)
cameraConfigurations.addAll( cameraConfigurations.addAll(
createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations)); matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, true, true, false));
}
// 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) {
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by windows-path & USB VID/PID only...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, true, true, true));
}
}
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by usb port & USB VID/PID...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, true, false, false));
}
// 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
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by base-name & usb port...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, false, true, false));
}
// handle disabling only-by-base-name matching
if (!matchCamerasOnlyByPath) {
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by base-name & USB VID/PID only...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, true, true, false));
}
// Legacy migration for if no USB VID/PID set
if (detectedCameraList.size() > 0 || unloadedConfigs.size() > 0) {
logger.info("Matching by base-name only...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, false, true, false));
}
} 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!"); logger.debug("Matched or created " + cameraConfigurations.size() + " camera configs!");
return cameraConfigurations; return cameraConfigurations;
} }
// loop over all the configs loaded from disk, attempting to match each camera /**
// to a config by path-by-id on linux * Abstractly match cameras
private List<CameraConfiguration> matchByPathByID( *
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) { * @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,
boolean checkUSBPath,
boolean checkVidPid,
boolean checkBaseName,
boolean checkPath) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>(); List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
List<CameraConfiguration> unloadedConfigsCopy = List<CameraConfiguration> unloadedConfigsCopy =
new ArrayList<CameraConfiguration>(unloadedConfigs); new ArrayList<CameraConfiguration>(unloadedConfigs);
@@ -262,111 +403,43 @@ public class VisionSourceManager {
for (CameraConfiguration config : unloadedConfigsCopy) { for (CameraConfiguration config : unloadedConfigsCopy) {
// Only run match path by id if the camera is not a CSI camera. // Only run match path by id if the camera is not a CSI camera.
if (config.cameraType != CameraType.ZeroCopyPicam) { if (config.cameraType != CameraType.ZeroCopyPicam) {
CameraInfo cameraInfo; logger.debug(
if (config.otherPaths.length == 0) { String.format(
logger.debug("No valid path-by-id found for config with name " + config.baseName); "Trying to find a match for loaded camera %s by strategy (path %s vid/pid %s basename %s path %s) with camera config: %s",
} else { config.baseName,
// attempt matching by path and basename checkUSBPath,
logger.debug( checkVidPid,
"Trying to find a match for loaded camera " checkBaseName,
+ config.baseName checkPath,
+ " with path-by-id " camCfgToString(config)));
+ config.otherPaths[0]);
cameraInfo =
detectedCamInfos.stream()
.filter(
usbCameraInfo ->
usbCameraInfo.otherPaths.length != 0
&& usbCameraInfo.otherPaths[0].equals(config.otherPaths[0])
&& usbCameraInfo.getBaseName().equals(config.baseName))
.findFirst()
.orElse(null);
// If we actually matched a camera to a config, remove that camera from the list // Get matcher and filter against it, picking out the first match
// and add it to the output Predicate<CameraInfo> matches =
if (cameraInfo != null) { getCameraMatcher(config, checkUSBPath, checkVidPid, checkBaseName, checkPath);
logger.debug("Matched the config for " + config.baseName + " to a physical camera!"); var cameraInfo = detectedCamInfos.stream().filter(matches).findFirst().orElse(null);
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo); // If we actually matched a camera to a config, remove that camera from the list
unloadedConfigs.remove(config); // and add it to the output
} if (cameraInfo != null) {
logger.debug("Matched the config for " + config.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
} else {
logger.debug("No camera found for the config " + config.baseName);
} }
} }
} }
return ret; return ret;
} }
private List<CameraConfiguration> matchByPath( /**
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) { * Create new {@link CameraConfiguration}s for unmatched cameras, and assign them a unique name
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>(); * (unique in the set of (loaded configs, unloaded configs, loaded vision modules) at least)
List<CameraConfiguration> unloadedConfigsCopy = */
new ArrayList<CameraConfiguration>(unloadedConfigs);
// now attempt to match the cameras and configs remaining by normal path
for (CameraConfiguration config : unloadedConfigsCopy) {
CameraInfo cameraInfo;
// attempt matching by path and basename
logger.debug(
"Trying to find a match for loaded camera "
+ config.baseName
+ " with path "
+ config.path);
cameraInfo =
detectedCamInfos.stream()
.filter(
usbCameraInfo ->
usbCameraInfo.path.equals(config.path)
&& usbCameraInfo.getBaseName().equals(config.baseName))
.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.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
}
}
return ret;
}
// Try matching cameras to configs by name.
private List<CameraConfiguration> matchByName(
List<CameraInfo> detectedCamInfos, List<CameraConfiguration> unloadedConfigs) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
List<CameraConfiguration> unloadedConfigsCopy =
new ArrayList<CameraConfiguration>(unloadedConfigs);
// if both path and ID based matching fails, attempt basename only match
for (CameraConfiguration config : unloadedConfigsCopy) {
CameraInfo cameraInfo;
logger.debug("Trying to find a match for loaded camera with name " + config.baseName);
cameraInfo =
detectedCamInfos.stream()
.filter(CameraInfo -> CameraInfo.getBaseName().equals(config.baseName))
.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.baseName + " to a physical camera!");
ret.add(mergeInfoIntoConfig(config, cameraInfo));
detectedCamInfos.remove(cameraInfo);
unloadedConfigs.remove(config);
}
}
return ret;
}
// If we have any unmatched cameras left, create a new CameraConfiguration for
// them here.
private List<CameraConfiguration> createConfigsForCameras( private List<CameraConfiguration> createConfigsForCameras(
List<CameraInfo> detectedCameraList, List<CameraInfo> detectedCameraList,
List<CameraConfiguration> loadedCamConfigs, List<CameraConfiguration> unloadedCamConfigs,
List<CameraConfiguration> loadedConfigs) { List<CameraConfiguration> loadedConfigs) {
List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>(); List<CameraConfiguration> ret = new ArrayList<CameraConfiguration>();
logger.debug( logger.debug(
@@ -377,7 +450,10 @@ public class VisionSourceManager {
String uniqueName = info.getHumanReadableName(); String uniqueName = info.getHumanReadableName();
int suffix = 0; int suffix = 0;
while (containsName(loadedConfigs, uniqueName) || containsName(uniqueName)) { while (containsName(loadedConfigs, uniqueName)
|| containsName(uniqueName)
|| containsName(unloadedCamConfigs, uniqueName)
|| containsName(ret, uniqueName)) {
suffix++; suffix++;
uniqueName = String.format("%s (%d)", uniqueName, suffix); uniqueName = String.format("%s (%d)", uniqueName, suffix);
} }
@@ -457,10 +533,16 @@ public class VisionSourceManager {
} }
private static List<VisionSource> loadVisionSourcesFromCamConfigs( private static List<VisionSource> loadVisionSourcesFromCamConfigs(
List<CameraConfiguration> camConfigs) { List<CameraConfiguration> camConfigs, boolean createSources) {
var cameraSources = new ArrayList<VisionSource>(); var cameraSources = new ArrayList<VisionSource>();
for (var configuration : camConfigs) { for (var configuration : camConfigs) {
logger.debug("Creating VisionSource for " + configuration); logger.debug("Creating VisionSource for " + camCfgToString(configuration));
// In unit tests, create dummy
if (!createSources) {
cameraSources.add(new TestSource(configuration));
continue;
}
boolean is_pi = Platform.isRaspberryPi(); boolean is_pi = Platform.isRaspberryPi();

View File

@@ -139,8 +139,31 @@ public class ConfigTest {
writer.write(str); writer.write(str);
writer.flush(); writer.flush();
writer.close(); writer.close();
Assertions.assertDoesNotThrow( CameraConfiguration result =
() -> JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class)); JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
tempFile.delete();
}
@Test
public void testJacksonAddUSBVIDPID() throws IOException {
var str =
"{\"baseName\":\"aaaaaa\",\"uniqueName\":\"aaaaaa\",\"nickname\":\"aaaaaa\",\"FOV\":70.0,\"path\":\"dev/vid\",\"cameraType\":\"UsbCamera\",\"currentPipelineIndex\":0,\"camPitch\":{\"radians\":0.0},\"calibrations\":[], \"usbVID\":3, \"usbPID\":4, \"cameraLEDs\":[]}";
File tempFile = File.createTempFile("test", ".json");
tempFile.deleteOnExit();
var writer = new FileWriter(tempFile);
writer.write(str);
writer.flush();
writer.close();
try {
CameraConfiguration result =
JacksonUtils.deserialize(tempFile.toPath(), CameraConfiguration.class);
String ser = JacksonUtils.serializeToString(result);
System.out.println(ser);
} catch (Exception e) {
e.printStackTrace();
}
tempFile.delete(); tempFile.delete();
} }

View File

@@ -84,7 +84,9 @@ public class SQLConfigTest {
CameraType.UsbCamera, CameraType.UsbCamera,
QuirkyCamera.getQuirkyCamera(-1, -1), QuirkyCamera.getQuirkyCamera(-1, -1),
List.of(), List.of(),
0); 0,
-1,
-1);
testcamcfg.pipelineSettings = testcamcfg.pipelineSettings =
List.of( List.of(
new ReflectivePipelineSettings(), new ReflectivePipelineSettings(),

View File

@@ -21,17 +21,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.CameraConfiguration; import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager; import org.photonvision.common.configuration.ConfigManager;
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.CameraInfo;
import org.photonvision.vision.camera.CameraType; import org.photonvision.vision.camera.CameraType;
public class VisionSourceManagerTest { public class VisionSourceManagerTest {
@Test @Test
public void visionSourceTest() { public void visionSourceTest() {
Logger.setLevel(LogGroup.Camera, LogLevel.DEBUG);
var inst = new VisionSourceManager(); var inst = new VisionSourceManager();
var cameraInfos = new ArrayList<CameraInfo>(); var cameraInfos = new ArrayList<CameraInfo>();
ConfigManager.getInstance().clearConfig();
ConfigManager.getInstance().load(); ConfigManager.getInstance().load();
inst.tryMatchCamImpl(cameraInfos); inst.tryMatchCamImpl(cameraInfos);
@@ -43,6 +50,8 @@ public class VisionSourceManagerTest {
"thirdTestVideo", "thirdTestVideo",
"dev/video1", "dev/video1",
new String[] {"by-id/123"}); new String[] {"by-id/123"});
config3.usbVID = 3;
config3.usbPID = 4;
var config4 = var config4 =
new CameraConfiguration( new CameraConfiguration(
"fourthTestVideo", "fourthTestVideo",
@@ -50,6 +59,8 @@ public class VisionSourceManagerTest {
"fourthTestVideo", "fourthTestVideo",
"dev/video2", "dev/video2",
new String[] {"by-id/321"}); new String[] {"by-id/321"});
config4.usbVID = 5;
config4.usbPID = 6;
CameraInfo info1 = new CameraInfo(0, "dev/video0", "testVideo", new String[0], 1, 2); CameraInfo info1 = new CameraInfo(0, "dev/video0", "testVideo", new String[0], 1, 2);
@@ -261,4 +272,268 @@ public class VisionSourceManagerTest {
assertEquals(10, inst.knownCameras.size()); assertEquals(10, inst.knownCameras.size());
assertEquals(0, inst.unmatchedLoadedConfigs.size()); assertEquals(0, inst.unmatchedLoadedConfigs.size());
} }
@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",
"fromt-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)",
"fromt-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
{
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);
// 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());
}
}
@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)",
"fromt-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)",
"fromt-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());
}
}
@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-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 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());
}
// duplciate 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-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 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());
}
} }

View File

@@ -90,7 +90,7 @@ class PhotonCamera:
retVal.populateFromPacket(pkt) retVal.populateFromPacket(pkt)
# NT4 allows us to correct the timestamp based on when the message was sent # NT4 allows us to correct the timestamp based on when the message was sent
retVal.setTimestampSeconds( retVal.setTimestampSeconds(
timestamp / 1e-6 - retVal.getLatencyMillis() / 1e-3 timestamp / 1e6 - retVal.getLatencyMillis() / 1e3
) )
return retVal return retVal

View File

@@ -408,8 +408,8 @@ public class PhotonPoseEstimator {
return Optional.empty(); return Optional.empty();
} }
if (estimatedPose.isEmpty()) { if (estimatedPose.isPresent()) {
lastPose = null; lastPose = estimatedPose.get().estimatedPose;
} }
return estimatedPose; return estimatedPose;

View File

@@ -431,7 +431,7 @@ public class PhotonCameraSim implements AutoCloseable {
detectableTgts.add( detectableTgts.add(
new PhotonTrackedTarget( new PhotonTrackedTarget(
Math.toDegrees(centerRot.getZ()), -Math.toDegrees(centerRot.getZ()),
-Math.toDegrees(centerRot.getY()), -Math.toDegrees(centerRot.getY()),
areaPercent, areaPercent,
Math.toDegrees(centerRot.getX()), Math.toDegrees(centerRot.getX()),

View File

@@ -186,6 +186,9 @@ std::optional<EstimatedRobotPose> PhotonPoseEstimator::Update(
ret = std::nullopt; ret = std::nullopt;
} }
if (ret) {
lastPose = ret.value().estimatedPose;
}
return ret; return ret;
} }

View File

@@ -260,7 +260,7 @@ class PhotonCameraSim {
std::vector<std::pair<double, double>> cornersDouble{cornersFloat.begin(), std::vector<std::pair<double, double>> cornersDouble{cornersFloat.begin(),
cornersFloat.end()}; cornersFloat.end()};
detectableTgts.emplace_back(PhotonTrackedTarget{ detectableTgts.emplace_back(PhotonTrackedTarget{
centerRot.Z().convert<units::degrees>().to<double>(), -centerRot.Z().convert<units::degrees>().to<double>(),
-centerRot.Y().convert<units::degrees>().to<double>(), areaPercent, -centerRot.Y().convert<units::degrees>().to<double>(), areaPercent,
centerRot.X().convert<units::degrees>().to<double>(), tgt.fiducialId, centerRot.X().convert<units::degrees>().to<double>(), tgt.fiducialId,
pnpSim.best, pnpSim.alt, pnpSim.ambiguity, smallVec, cornersDouble}); pnpSim.best, pnpSim.alt, pnpSim.ambiguity, smallVec, cornersDouble});

View File

@@ -256,7 +256,8 @@ class VisionSystemSimTest {
cameraSim.setMinTargetAreaPixels(0.0); cameraSim.setMinTargetAreaPixels(0.0);
visionSysSim.addVisionTargets(new VisionTargetSim(targetPose, new TargetModel(0.5, 0.5), 3)); visionSysSim.addVisionTargets(new VisionTargetSim(targetPose, new TargetModel(0.5, 0.5), 3));
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(-1.0 * testYaw)); // If the robot is rotated x deg (CCW+), the target yaw should be x deg (CW+)
var robotPose = new Pose2d(new Translation2d(10, 0), Rotation2d.fromDegrees(testYaw));
visionSysSim.update(robotPose); visionSysSim.update(robotPose);
var res = camera.getLatestResult(); var res = camera.getLatestResult();
assertTrue(res.hasTargets()); assertTrue(res.hasTargets());

View File

@@ -220,8 +220,9 @@ TEST_P(VisionSystemSimTestWithParamsTest, YawAngles) {
visionSysSim.AddVisionTargets({photon::VisionTargetSim{ visionSysSim.AddVisionTargets({photon::VisionTargetSim{
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}}); targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}});
robotPose = frc::Pose2d{frc::Translation2d{10_m, 0_m}, // If the robot is rotated x deg (CCW+), the target yaw should be x deg (CW+)
frc::Rotation2d{-1 * GetParam()}}; robotPose =
frc::Pose2d{frc::Translation2d{10_m, 0_m}, frc::Rotation2d{GetParam()}};
visionSysSim.Update(robotPose); visionSysSim.Update(robotPose);
ASSERT_TRUE(camera.GetLatestResult().HasTargets()); ASSERT_TRUE(camera.GetLatestResult().HasTargets());
ASSERT_NEAR(GetParam().to<double>(), ASSERT_NEAR(GetParam().to<double>(),

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }

View File

@@ -39,8 +39,8 @@ class Robot : public frc::TimedRobot {
void TeleopPeriodic() override; void TeleopPeriodic() override;
private: private:
// Change this to match the name of your camera // Change this to match the name of your camera as shown in the web UI
photon::PhotonCamera camera{"photonvision"}; photon::PhotonCamera camera{"YOUR_CAMERA_NAME_HERE"};
// PID constants should be tuned per robot // PID constants should be tuned per robot
frc::PIDController controller{.1, 0, 0}; frc::PIDController controller{.1, 0, 0};

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }

View File

@@ -1,7 +1,7 @@
plugins { plugins {
id "cpp" id "cpp"
id "google-test-test-suite" id "google-test-test-suite"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
id "com.dorongold.task-tree" version "2.1.0" id "com.dorongold.task-tree" version "2.1.0"
} }
@@ -12,8 +12,8 @@ repositories {
} }
wpi.maven.useDevelopment = true wpi.maven.useDevelopment = true
wpi.versions.wpilibVersion = "2024.2.1" wpi.versions.wpilibVersion = "2024.3.1"
wpi.versions.wpimathVersion = "2024.2.1" wpi.versions.wpimathVersion = "2024.3.1"
apply from: "${rootDir}/../shared/examples_common.gradle" apply from: "${rootDir}/../shared/examples_common.gradle"

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -48,8 +48,8 @@ public class Robot extends TimedRobot {
// How far from the target we want to be // How far from the target we want to be
final double GOAL_RANGE_METERS = Units.feetToMeters(3); final double GOAL_RANGE_METERS = Units.feetToMeters(3);
// Change this to match the name of your camera // Change this to match the name of your camera as shown in the web UI
PhotonCamera camera = new PhotonCamera("photonvision"); PhotonCamera camera = new PhotonCamera("YOUR_CAMERA_NAME_HERE");
// PID constants should be tuned per robot // PID constants should be tuned per robot
final double LINEAR_P = 0.1; final double LINEAR_P = 0.1;

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11

View File

@@ -1,6 +1,6 @@
plugins { plugins {
id "java" id "java"
id "edu.wpi.first.GradleRIO" version "2024.2.1" id "edu.wpi.first.GradleRIO" version "2024.3.1"
} }
sourceCompatibility = JavaVersion.VERSION_11 sourceCompatibility = JavaVersion.VERSION_11
@@ -11,8 +11,8 @@ apply from: "${rootDir}/../shared/examples_common.gradle"
def ROBOT_MAIN_CLASS = "frc.robot.Main" def ROBOT_MAIN_CLASS = "frc.robot.Main"
wpi.maven.useDevelopment = true wpi.maven.useDevelopment = true
wpi.versions.wpilibVersion = "2024.2.1" wpi.versions.wpilibVersion = "2024.3.1"
wpi.versions.wpimathVersion = "2024.2.1" wpi.versions.wpimathVersion = "2024.3.1"
// Define my targets (RoboRIO) and artifacts (deployable files) // Define my targets (RoboRIO) and artifacts (deployable files)

View File

@@ -80,6 +80,9 @@ if [[ "$DISTRO" = "Ubuntu" && "$INSTALL_NETWORK_MANAGER" != "true" && -z "$QUIET
fi fi
fi fi
echo "Update package list"
apt-get update
echo "Installing curl..." echo "Installing curl..."
apt-get install --yes curl apt-get install --yes curl
echo "curl installation complete." echo "curl installation complete."
@@ -136,6 +139,9 @@ echo "Installing v4l-utils..."
apt-get install --yes v4l-utils apt-get install --yes v4l-utils
echo "v4l-utils installation complete." echo "v4l-utils installation complete."
echo "Installing sqlite3"
apt-get install --yes sqlite3
echo "Downloading latest stable release of PhotonVision..." echo "Downloading latest stable release of PhotonVision..."
mkdir -p /opt/photonvision mkdir -p /opt/photonvision
cd /opt/photonvision cd /opt/photonvision