diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 2eb33d47c3..709c50d491 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -290,13 +290,13 @@ jobs:
- os: ubuntu-latest
artifact-name: LinuxArm64
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
image_additional_mb: 4096
- os: ubuntu-latest
artifact-name: LinuxArm64
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
image_additional_mb: 4096
diff --git a/build.gradle b/build.gradle
index a9671e75df..d1b9cbf706 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,7 +4,7 @@ plugins {
id "com.diffplug.spotless" version "6.24.0"
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.GradleRIO" version "2024.2.1"
+ id "edu.wpi.first.GradleRIO" version "2024.3.1"
id 'edu.wpi.first.WpilibTools' version '1.3.0'
id 'com.google.protobuf' version '0.9.4' apply false
}
@@ -24,7 +24,7 @@ allprojects {
apply from: "versioningHelper.gradle"
ext {
- wpilibVersion = "2024.2.1"
+ wpilibVersion = "2024.3.1"
wpimathVersion = wpilibVersion
openCVversion = "4.8.0-2"
joglVersion = "2.4.0-rc-20200307"
diff --git a/photon-client/src/components/settings/NetworkingCard.vue b/photon-client/src/components/settings/NetworkingCard.vue
index 1b3b7bb566..e3b80e2d99 100644
--- a/photon-client/src/components/settings/NetworkingCard.vue
+++ b/photon-client/src/components/settings/NetworkingCard.vue
@@ -281,11 +281,29 @@ watchEffect(() => {
+
+ 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.
+
+ 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.
+
+ To add a new camera to this coprocessor, disable this setting, connect the camera, and re-enable.
+
This also disables creating new CameraConfigurations for detected "new" cameras.
*/
public boolean matchCamerasOnlyByPath = false;
diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/CameraInfo.java b/photon-core/src/main/java/org/photonvision/vision/camera/CameraInfo.java
index e7a9f4b04a..8f5893401b 100644
--- a/photon-core/src/main/java/org/photonvision/vision/camera/CameraInfo.java
+++ b/photon-core/src/main/java/org/photonvision/vision/camera/CameraInfo.java
@@ -20,6 +20,7 @@
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;
@@ -80,15 +81,27 @@ public Optional getUSBPath() {
}
@Override
- public boolean equals(Object o) {
- if (o == this) return true;
- if (!(o instanceof UsbCameraInfo || o instanceof CameraInfo)) return false;
- UsbCameraInfo other = (UsbCameraInfo) o;
- return path.equals(other.path)
- // && a.dev == b.dev (dev is not constant in Windows)
- && name.equals(other.name)
- && productId == other.productId
- && vendorId == other.vendorId;
+ 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.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
@@ -101,6 +114,8 @@ public String toString() {
+ vendorId
+ ", pid="
+ productId
+ + ", path="
+ + path
+ ", otherPaths="
+ Arrays.toString(otherPaths)
+ "]";
diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/TestSource.java b/photon-core/src/main/java/org/photonvision/vision/camera/TestSource.java
new file mode 100644
index 0000000000..51a5cef580
--- /dev/null
+++ b/photon-core/src/main/java/org/photonvision/vision/camera/TestSource.java
@@ -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 .
+ */
+
+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'");
+ }
+}
diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/USBCameraSource.java b/photon-core/src/main/java/org/photonvision/vision/camera/USBCameraSource.java
index 1a29a9af2f..dbecd1ef5d 100644
--- a/photon-core/src/main/java/org/photonvision/vision/camera/USBCameraSource.java
+++ b/photon-core/src/main/java/org/photonvision/vision/camera/USBCameraSource.java
@@ -57,8 +57,8 @@ public USBCameraSource(CameraConfiguration config) {
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.usbVID <= 0) config.usbVID = this.camera.getInfo().vendorId;
+ if (config.usbPID <= 0) config.usbPID = this.camera.getInfo().productId;
if (getCameraConfiguration().cameraQuirks == null)
getCameraConfiguration().cameraQuirks =
diff --git a/photon-core/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java b/photon-core/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java
index d5ceda85a8..b7a8aa5b39 100644
--- a/photon-core/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java
+++ b/photon-core/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java
@@ -39,6 +39,7 @@
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.USBCameraSource;
public class VisionSourceManager {
@@ -146,8 +147,8 @@ protected List tryMatchCamImpl(ArrayList cameraInfos)
}
// Return no new sources because there are no new sources
- if (connectedCameras.isEmpty() && !cameraInfos.isEmpty()) {
- if (hasWarnedNoCameras) {
+ 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;
@@ -186,7 +187,7 @@ protected List tryMatchCamImpl(ArrayList cameraInfos)
"Unloaded configs: "
+ unmatchedLoadedConfigs.stream()
.map(it -> it.nickname)
- .collect(Collectors.joining()));
+ .collect(Collectors.joining(", ")));
hasWarned = true;
}
@@ -195,13 +196,8 @@ protected List tryMatchCamImpl(ArrayList cameraInfos)
if (matchedCameras.isEmpty()) return null;
- // for unit tests only!
- if (!createSources) {
- return List.of();
- }
-
// Turn these camera configs into vision sources
- var sources = loadVisionSourcesFromCamConfigs(matchedCameras);
+ var sources = loadVisionSourcesFromCamConfigs(matchedCameras, createSources);
// Print info about each vision source
for (var src : sources) {
@@ -317,26 +313,32 @@ public List matchCameras(
logger.info("Matching by usb port & name & USB VID/PID...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, true, true, true, false));
- } else
- logger.debug("Skipping match by usb port/name/vid/pid, no configs or cameras left to match");
+ }
// 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()) {
+ 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));
- } else
- logger.debug(
- "Skipping matching by windiws-path/name/vid/pid, no configs or cameras left to match");
+ }
}
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));
- } else logger.debug("Skipping match by port/vid/pid, no configs or cameras left to match");
+ }
+
+ // 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) {
@@ -344,13 +346,29 @@ public List matchCameras(
logger.info("Matching by base-name & USB VID/PID only...");
cameraConfigurations.addAll(
matchCamerasByStrategy(detectedCameraList, unloadedConfigs, false, true, true, false));
- } else
- logger.debug("Skipping match by base-name/viid/pid, no configs or cameras left to match");
+ }
+
+ // 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) {
- cameraConfigurations.addAll(
- createConfigsForCameras(detectedCameraList, unloadedConfigs, cameraConfigurations));
+ // 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!");
@@ -434,7 +452,8 @@ private List createConfigsForCameras(
int suffix = 0;
while (containsName(loadedConfigs, uniqueName)
|| containsName(uniqueName)
- || containsName(unloadedCamConfigs, uniqueName)) {
+ || containsName(unloadedCamConfigs, uniqueName)
+ || containsName(ret, uniqueName)) {
suffix++;
uniqueName = String.format("%s (%d)", uniqueName, suffix);
}
@@ -514,11 +533,17 @@ private List filterAllowedDevices(List allDevices) {
}
private static List loadVisionSourcesFromCamConfigs(
- List camConfigs) {
+ List camConfigs, boolean createSources) {
var cameraSources = new ArrayList();
for (var configuration : camConfigs) {
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();
if (configuration.cameraType == CameraType.ZeroCopyPicam && is_pi) {
diff --git a/photon-core/src/test/java/org/photonvision/vision/processes/VisionSourceManagerTest.java b/photon-core/src/test/java/org/photonvision/vision/processes/VisionSourceManagerTest.java
index 3f92b0e718..f82993e05f 100644
--- a/photon-core/src/test/java/org/photonvision/vision/processes/VisionSourceManagerTest.java
+++ b/photon-core/src/test/java/org/photonvision/vision/processes/VisionSourceManagerTest.java
@@ -21,6 +21,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.ArrayList;
+import java.util.List;
import org.junit.jupiter.api.Test;
import org.photonvision.common.configuration.CameraConfiguration;
import org.photonvision.common.configuration.ConfigManager;
@@ -271,4 +272,268 @@ public void visionSourceTest() {
assertEquals(10, inst.knownCameras.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();
+ cameraInfos.add(info1);
+ cameraInfos.add(info2);
+ List 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();
+ cameraInfos.add(info1);
+ cameraInfos.add(info2);
+ List 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();
+ cameraInfos.add(info1);
+ cameraInfos.add(info2);
+ List 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();
+
+ 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 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());
+ }
}
diff --git a/photon-lib/src/main/java/org/photonvision/simulation/PhotonCameraSim.java b/photon-lib/src/main/java/org/photonvision/simulation/PhotonCameraSim.java
index 8b6db02314..60a1ae2d05 100644
--- a/photon-lib/src/main/java/org/photonvision/simulation/PhotonCameraSim.java
+++ b/photon-lib/src/main/java/org/photonvision/simulation/PhotonCameraSim.java
@@ -431,7 +431,7 @@ public PhotonPipelineResult process(
detectableTgts.add(
new PhotonTrackedTarget(
- Math.toDegrees(centerRot.getZ()),
+ -Math.toDegrees(centerRot.getZ()),
-Math.toDegrees(centerRot.getY()),
areaPercent,
Math.toDegrees(centerRot.getX()),
diff --git a/photon-lib/src/main/native/include/photon/simulation/PhotonCameraSim.h b/photon-lib/src/main/native/include/photon/simulation/PhotonCameraSim.h
index 9b7397634b..adcd9558c6 100644
--- a/photon-lib/src/main/native/include/photon/simulation/PhotonCameraSim.h
+++ b/photon-lib/src/main/native/include/photon/simulation/PhotonCameraSim.h
@@ -260,7 +260,7 @@ class PhotonCameraSim {
std::vector> cornersDouble{cornersFloat.begin(),
cornersFloat.end()};
detectableTgts.emplace_back(PhotonTrackedTarget{
- centerRot.Z().convert().to(),
+ -centerRot.Z().convert().to(),
-centerRot.Y().convert().to(), areaPercent,
centerRot.X().convert().to(), tgt.fiducialId,
pnpSim.best, pnpSim.alt, pnpSim.ambiguity, smallVec, cornersDouble});
diff --git a/photon-lib/src/test/java/org/photonvision/VisionSystemSimTest.java b/photon-lib/src/test/java/org/photonvision/VisionSystemSimTest.java
index e0ffce7abf..71cd586546 100644
--- a/photon-lib/src/test/java/org/photonvision/VisionSystemSimTest.java
+++ b/photon-lib/src/test/java/org/photonvision/VisionSystemSimTest.java
@@ -256,7 +256,8 @@ public void testYawAngles(double testYaw) {
cameraSim.setMinTargetAreaPixels(0.0);
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);
var res = camera.getLatestResult();
assertTrue(res.hasTargets());
diff --git a/photon-lib/src/test/native/cpp/VisionSystemSimTest.cpp b/photon-lib/src/test/native/cpp/VisionSystemSimTest.cpp
index 1c3f9eac58..667b6f53af 100644
--- a/photon-lib/src/test/native/cpp/VisionSystemSimTest.cpp
+++ b/photon-lib/src/test/native/cpp/VisionSystemSimTest.cpp
@@ -220,8 +220,9 @@ TEST_P(VisionSystemSimTestWithParamsTest, YawAngles) {
visionSysSim.AddVisionTargets({photon::VisionTargetSim{
targetPose, photon::TargetModel{0.5_m, 0.5_m}, 3}});
- robotPose = frc::Pose2d{frc::Translation2d{10_m, 0_m},
- frc::Rotation2d{-1 * GetParam()}};
+ // If the robot is rotated x deg (CCW+), the target yaw should be x deg (CW+)
+ robotPose =
+ frc::Pose2d{frc::Translation2d{10_m, 0_m}, frc::Rotation2d{GetParam()}};
visionSysSim.Update(robotPose);
ASSERT_TRUE(camera.GetLatestResult().HasTargets());
ASSERT_NEAR(GetParam().to(),
diff --git a/photonlib-cpp-examples/aimandrange/build.gradle b/photonlib-cpp-examples/aimandrange/build.gradle
index 01af7203ac..c16ba821ca 100644
--- a/photonlib-cpp-examples/aimandrange/build.gradle
+++ b/photonlib-cpp-examples/aimandrange/build.gradle
@@ -1,7 +1,7 @@
plugins {
id "cpp"
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"
}
diff --git a/photonlib-cpp-examples/aimattarget/build.gradle b/photonlib-cpp-examples/aimattarget/build.gradle
index 01af7203ac..c16ba821ca 100644
--- a/photonlib-cpp-examples/aimattarget/build.gradle
+++ b/photonlib-cpp-examples/aimattarget/build.gradle
@@ -1,7 +1,7 @@
plugins {
id "cpp"
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"
}
diff --git a/photonlib-cpp-examples/apriltagExample/build.gradle b/photonlib-cpp-examples/apriltagExample/build.gradle
index 01af7203ac..c16ba821ca 100644
--- a/photonlib-cpp-examples/apriltagExample/build.gradle
+++ b/photonlib-cpp-examples/apriltagExample/build.gradle
@@ -1,7 +1,7 @@
plugins {
id "cpp"
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"
}
diff --git a/photonlib-cpp-examples/getinrange/build.gradle b/photonlib-cpp-examples/getinrange/build.gradle
index 01af7203ac..c16ba821ca 100644
--- a/photonlib-cpp-examples/getinrange/build.gradle
+++ b/photonlib-cpp-examples/getinrange/build.gradle
@@ -1,7 +1,7 @@
plugins {
id "cpp"
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"
}
diff --git a/photonlib-cpp-examples/swervedriveposeestsim/build.gradle b/photonlib-cpp-examples/swervedriveposeestsim/build.gradle
index 41a914fcce..19be42708e 100644
--- a/photonlib-cpp-examples/swervedriveposeestsim/build.gradle
+++ b/photonlib-cpp-examples/swervedriveposeestsim/build.gradle
@@ -1,7 +1,7 @@
plugins {
id "cpp"
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"
}
@@ -12,8 +12,8 @@ repositories {
}
wpi.maven.useDevelopment = true
-wpi.versions.wpilibVersion = "2024.2.1"
-wpi.versions.wpimathVersion = "2024.2.1"
+wpi.versions.wpilibVersion = "2024.3.1"
+wpi.versions.wpimathVersion = "2024.3.1"
apply from: "${rootDir}/../shared/examples_common.gradle"
diff --git a/photonlib-java-examples/aimandrange/build.gradle b/photonlib-java-examples/aimandrange/build.gradle
index 1ed5c6a664..1d5fd3e540 100644
--- a/photonlib-java-examples/aimandrange/build.gradle
+++ b/photonlib-java-examples/aimandrange/build.gradle
@@ -1,6 +1,6 @@
plugins {
id "java"
- id "edu.wpi.first.GradleRIO" version "2024.2.1"
+ id "edu.wpi.first.GradleRIO" version "2024.3.1"
}
sourceCompatibility = JavaVersion.VERSION_11
diff --git a/photonlib-java-examples/aimattarget/build.gradle b/photonlib-java-examples/aimattarget/build.gradle
index 73646cba1f..04daddd4a7 100644
--- a/photonlib-java-examples/aimattarget/build.gradle
+++ b/photonlib-java-examples/aimattarget/build.gradle
@@ -1,6 +1,6 @@
plugins {
id "java"
- id "edu.wpi.first.GradleRIO" version "2024.2.1"
+ id "edu.wpi.first.GradleRIO" version "2024.3.1"
}
sourceCompatibility = JavaVersion.VERSION_11
diff --git a/photonlib-java-examples/getinrange/build.gradle b/photonlib-java-examples/getinrange/build.gradle
index 1ed5c6a664..1d5fd3e540 100644
--- a/photonlib-java-examples/getinrange/build.gradle
+++ b/photonlib-java-examples/getinrange/build.gradle
@@ -1,6 +1,6 @@
plugins {
id "java"
- id "edu.wpi.first.GradleRIO" version "2024.2.1"
+ id "edu.wpi.first.GradleRIO" version "2024.3.1"
}
sourceCompatibility = JavaVersion.VERSION_11
diff --git a/photonlib-java-examples/simaimandrange/build.gradle b/photonlib-java-examples/simaimandrange/build.gradle
index 1ed5c6a664..1d5fd3e540 100644
--- a/photonlib-java-examples/simaimandrange/build.gradle
+++ b/photonlib-java-examples/simaimandrange/build.gradle
@@ -1,6 +1,6 @@
plugins {
id "java"
- id "edu.wpi.first.GradleRIO" version "2024.2.1"
+ id "edu.wpi.first.GradleRIO" version "2024.3.1"
}
sourceCompatibility = JavaVersion.VERSION_11
diff --git a/photonlib-java-examples/swervedriveposeestsim/build.gradle b/photonlib-java-examples/swervedriveposeestsim/build.gradle
index 5d3b76a38d..6cab518ff7 100644
--- a/photonlib-java-examples/swervedriveposeestsim/build.gradle
+++ b/photonlib-java-examples/swervedriveposeestsim/build.gradle
@@ -1,6 +1,6 @@
plugins {
id "java"
- id "edu.wpi.first.GradleRIO" version "2024.2.1"
+ id "edu.wpi.first.GradleRIO" version "2024.3.1"
}
sourceCompatibility = JavaVersion.VERSION_11
@@ -11,8 +11,8 @@ apply from: "${rootDir}/../shared/examples_common.gradle"
def ROBOT_MAIN_CLASS = "frc.robot.Main"
wpi.maven.useDevelopment = true
-wpi.versions.wpilibVersion = "2024.2.1"
-wpi.versions.wpimathVersion = "2024.2.1"
+wpi.versions.wpilibVersion = "2024.3.1"
+wpi.versions.wpimathVersion = "2024.3.1"
// Define my targets (RoboRIO) and artifacts (deployable files)
diff --git a/scripts/install.sh b/scripts/install.sh
index 985b61ea99..fe845d87c0 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -80,6 +80,9 @@ if [[ "$DISTRO" = "Ubuntu" && "$INSTALL_NETWORK_MANAGER" != "true" && -z "$QUIET
fi
fi
+echo "Update package list"
+apt-get update
+
echo "Installing curl..."
apt-get install --yes curl
echo "curl installation complete."
@@ -136,6 +139,9 @@ echo "Installing v4l-utils..."
apt-get install --yes v4l-utils
echo "v4l-utils installation complete."
+echo "Installing sqlite3"
+apt-get install --yes sqlite3
+
echo "Downloading latest stable release of PhotonVision..."
mkdir -p /opt/photonvision
cd /opt/photonvision