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