diff --git a/photon-core/src/main/java/org/photonvision/raspi/LibCameraJNI.java b/photon-core/src/main/java/org/photonvision/raspi/LibCameraJNI.java index a75ae2e579..8a12aeabfa 100644 --- a/photon-core/src/main/java/org/photonvision/raspi/LibCameraJNI.java +++ b/photon-core/src/main/java/org/photonvision/raspi/LibCameraJNI.java @@ -64,6 +64,7 @@ public enum SensorModel { Disconnected, OV5647, // Picam v1 IMX219, // Picam v2 + IMX708, // Picam v3 IMX477, // Picam HQ OV9281, OV7251, @@ -77,6 +78,8 @@ public String getFriendlyName() { return "Camera Module v1"; case IMX219: return "Camera Module v2"; + case IMX708: + return "Camera Module v3"; case IMX477: return "HQ Camera"; case OV9281: diff --git a/photon-core/src/main/java/org/photonvision/vision/camera/LibcameraGpuSettables.java b/photon-core/src/main/java/org/photonvision/vision/camera/LibcameraGpuSettables.java index 7e0bd5c5c7..2fdde391d2 100644 --- a/photon-core/src/main/java/org/photonvision/vision/camera/LibcameraGpuSettables.java +++ b/photon-core/src/main/java/org/photonvision/vision/camera/LibcameraGpuSettables.java @@ -86,6 +86,9 @@ public LibcameraGpuSettables(CameraConfiguration configuration) { if (sensorModel == LibCameraJNI.SensorModel.IMX477) { LibcameraGpuSource.logger.warn( "It appears you are using a Pi HQ Camera. This camera is not officially supported. You will have to set your camera FOV differently based on resolution."); + } else if (sensorModel == LibCameraJNI.SensorModel.IMX708) { + LibcameraGpuSource.logger.warn( + "It appears you are using a Pi Camera V3. This camera is not officially supported. You will have to set your camera FOV differently based on resolution."); } else if (sensorModel == LibCameraJNI.SensorModel.Unknown) { LibcameraGpuSource.logger.warn( "You have an unknown sensor connected to your Pi over CSI! This is likely a bug. If it is not, then you will have to set your camera FOV differently based on resolution."); 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 2e6ada0aa5..970b1a6ae7 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 @@ -142,7 +142,15 @@ protected List tryMatchUSBCamImpl(boolean createSources) { logger.debug("Trying to match " + usbCamConfigs.size() + " unmatched configs..."); // Match camera configs to physical cameras - var matchedCameras = matchUSBCameras(notYetLoadedCams, unmatchedLoadedConfigs); + + List matchedCameras; + + if (!createSources) { + matchedCameras = matchUSBCameras(notYetLoadedCams, unmatchedLoadedConfigs, false); + } else { + matchedCameras = matchUSBCameras(notYetLoadedCams, unmatchedLoadedConfigs); + } + unmatchedLoadedConfigs.removeAll(matchedCameras); if (!unmatchedLoadedConfigs.isEmpty() && !hasWarned) { logger.warn( @@ -198,6 +206,22 @@ protected List tryMatchUSBCamImpl(boolean createSources) { */ protected List matchUSBCameras( List detectedCamInfos, List loadedUsbCamConfigs) { + return matchUSBCameras(detectedCamInfos, loadedUsbCamConfigs, true); + } + + /** + * Create {@link CameraConfiguration}s based on a list of detected USB cameras and the configs on + * disk. + * + * @param detectedCamInfos Information about currently connected USB cameras. + * @param loadedUsbCamConfigs The USB {@link CameraConfiguration}s loaded from disk. + * @param useJNI If false, this is a unit test and the JNI should not be used for CSI devices. + * @return the matched configurations. + */ + private List matchUSBCameras( + List detectedCamInfos, + List loadedUsbCamConfigs, + boolean useJNI) { var detectedCameraList = new ArrayList<>(detectedCamInfos); ArrayList cameraConfigurations = new ArrayList<>(); @@ -320,7 +344,11 @@ && cameraNameToBaseName(usbCameraInfo.name).equals(config.baseName)) // HACK -- for picams, we want to use the camera model String nickname = uniqueName; if (isCsiCamera(info)) { - nickname = LibCameraJNI.getSensorModel().toString(); + if (useJNI) { + nickname = LibCameraJNI.getSensorModel().toString(); + } else { + nickname = "CSICAM-DEV"; + } } CameraConfiguration configuration = @@ -342,6 +370,7 @@ private CameraConfiguration mergeInfoIntoConfig(CameraConfiguration cfg, UsbCame logger.debug("Updating path config from " + cfg.path + " to " + info.path); cfg.path = info.path; } + cfg.otherPaths = info.otherPaths; if (cfg.otherPaths.length != info.otherPaths.length) { logger.debug( @@ -373,16 +402,56 @@ public void setIgnoredCamerasRegex(String ignoredCamerasRegex) { private List filterAllowedDevices(List allDevices) { List filteredDevices = new ArrayList<>(); + List badDevices = new ArrayList<>(); + for (var device : allDevices) { + // Filter devices that are physically the same device but may show up as multiple devices that + // really cant be accessed. First noticed with raspi 5 and ov5647. + + List paths = new ArrayList<>(); + + boolean skip = false; + if (device.otherPaths.length != 0) { + // Use the other paths to filter out devices that share the same path other than the index + // select only the lowest index. + // A ov5647 on a raspi 5 would show another path as + // platform-1000880000.pisp_be-video-index0, + // platform-1000880000.pisp_be-video-index4, and platform-1000880000.pisp_be-video-index5. + // This code will remove "indexX" from all the other paths from all the devices and make + // sure + // that we only take one camera stream from each device the stream with the lowest index. + for (String p : device.otherPaths) { + paths.add(p.split("index")[0]); + } + for (var otherDevice : filteredDevices) { + if (otherDevice.otherPaths.length == 0) continue; + List otherPaths = new ArrayList<>(); + for (String p : otherDevice.otherPaths) { + otherPaths.add(p.split("index")[0]); + } + if (paths.containsAll(otherPaths)) { + if (otherDevice.dev >= device.dev) { + badDevices.add(otherDevice); + } else { + skip = true; + break; + } + } + } + } + + filteredDevices.removeAll(badDevices); if (deviceBlacklist.contains(device.name)) { logger.trace( "Skipping blacklisted device: \"" + device.name + "\" at \"" + device.path + "\""); } else if (device.name.matches(ignoredCamerasRegex)) { logger.trace("Skipping ignored device: \"" + device.name + "\" at \"" + device.path); - } else { + } else if (!skip) { filteredDevices.add(device); logger.trace( "Adding local video device - \"" + device.name + "\" at \"" + device.path + "\""); + } else { + logger.trace("Skipping duplicate device: \"" + device.name + "\" at \"" + device.path); } } return filteredDevices; 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 e16213000e..4a995d26fd 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 @@ -35,18 +35,19 @@ public void visionSourceTest() { ConfigManager.getInstance().load(); inst.tryMatchUSBCamImpl(); + var config3 = new CameraConfiguration( - "secondTestVideo", - "secondTestVideo1", - "secondTestVideo1", + "thirdTestVideo", + "thirdTestVideo", + "thirdTestVideo", "dev/video1", new String[] {"by-id/123"}); var config4 = new CameraConfiguration( - "secondTestVideo", - "secondTestVideo2", - "secondTestVideo2", + "fourthTestVideo", + "fourthTestVideo", + "fourthTestVideo", "dev/video2", new String[] {"by-id/321"}); @@ -61,7 +62,8 @@ public void visionSourceTest() { assertTrue(inst.knownUsbCameras.contains(info1)); assertEquals(2, inst.unmatchedLoadedConfigs.size()); - UsbCameraInfo info2 = new UsbCameraInfo(0, "dev/video1", "testVideo", new String[0], 1, 2); + UsbCameraInfo info2 = + new UsbCameraInfo(0, "dev/video1", "secondTestVideo", new String[0], 2, 3); infoList.add(info2); @@ -75,10 +77,10 @@ public void visionSourceTest() { assertEquals(2, inst.unmatchedLoadedConfigs.size()); UsbCameraInfo info3 = - new UsbCameraInfo(0, "dev/video2", "secondTestVideo", new String[] {"by-id/123"}, 2, 1); + new UsbCameraInfo(0, "dev/video2", "thirdTestVideo", new String[] {"by-id/123"}, 3, 4); UsbCameraInfo info4 = - new UsbCameraInfo(0, "dev/video3", "secondTestVideo", new String[] {"by-id/321"}, 3, 1); + new UsbCameraInfo(0, "dev/video3", "fourthTestVideo", new String[] {"by-id/321"}, 5, 6); infoList.add(info4); @@ -118,7 +120,80 @@ public void visionSourceTest() { assertEquals(cam3.nickname, config3.nickname); assertEquals(cam4.nickname, config4.nickname); - assertEquals(4, inst.knownUsbCameras.size()); + + UsbCameraInfo info5 = + new UsbCameraInfo( + 2, + "/dev/video2", + "Left Camera", + new String[] { + "/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Left_Camera_12345-video-index0", + "/dev/v4l/by-path/platform-xhci-hcd.0-usb-0:2:1.0-video-index0" + }, + 7, + 8); + infoList.add(info5); + inst.tryMatchUSBCamImpl(false); + + assertTrue(inst.knownUsbCameras.contains(info5)); + + UsbCameraInfo info6 = + new UsbCameraInfo( + 3, + "dev/video3", + "Right Camera", + new String[] { + "/dev/v4l/by-id/usb-Arducam_Technology_Co.__Ltd._Right_Camera_123456-video-index0", + "/dev/v4l/by-path/platform-xhci-hcd.1-usb-0:1:1.0-video-index0" + }, + 9, + 10); + infoList.add(info6); + inst.tryMatchUSBCamImpl(false); + + assertTrue(inst.knownUsbCameras.contains(info6)); + + // RPI 5 CSI Tests + + UsbCameraInfo info7 = + new UsbCameraInfo( + 4, + "dev/video4", + "CSICAM-DEV", // Typically rp1-cfe for unit test changed to CSICAM-DEV + new String[] {"/dev/v4l/by-path/platform-1f00110000.csi-video-index0"}, + 11, + 12); + infoList.add(info7); + inst.tryMatchUSBCamImpl(false); + + assertTrue(inst.knownUsbCameras.contains(info7)); + + UsbCameraInfo info8 = + new UsbCameraInfo( + 5, + "dev/video8", + "CSICAM-DEV", // Typically rp1-cfe for unit test changed to CSICAM-DEV + new String[] {"/dev/v4l/by-path/platform-1f00110000.csi-video-index4"}, + 13, + 14); + infoList.add(info8); + inst.tryMatchUSBCamImpl(false); + + assertTrue(!inst.knownUsbCameras.contains(info8)); // This camera should not be recognized/used. + + UsbCameraInfo info9 = + new UsbCameraInfo( + 6, + "dev/video9", + "CSICAM-DEV", // Typically rp1-cfe for unit test changed to CSICAM-DEV + new String[] {"/dev/v4l/by-path/platform-1f00110000.csi-video-index5"}, + 15, + 16); + infoList.add(info9); + inst.tryMatchUSBCamImpl(false); + + assertTrue(!inst.knownUsbCameras.contains(info9)); // This camera should not be recognized/used. + assertEquals(7, inst.knownUsbCameras.size()); assertEquals(0, inst.unmatchedLoadedConfigs.size()); } } diff --git a/photon-server/src/main/resources/nativelibraries/libphotonlibcamera.so b/photon-server/src/main/resources/nativelibraries/libphotonlibcamera.so index 90bb006943..c2d0d9b496 100644 Binary files a/photon-server/src/main/resources/nativelibraries/libphotonlibcamera.so and b/photon-server/src/main/resources/nativelibraries/libphotonlibcamera.so differ