diff --git a/photon-server/src/main/java/org/photonvision/Main.java b/photon-server/src/main/java/org/photonvision/Main.java index 5d9b2eeb3..72bc20810 100644 --- a/photon-server/src/main/java/org/photonvision/Main.java +++ b/photon-server/src/main/java/org/photonvision/Main.java @@ -80,13 +80,12 @@ public class Main { var usbSrc = (USBCameraSource) src; collectedSources.put(usbSrc, usbSrc.configuration.pipelineSettings); logger.trace( - () -> { - return "Matched config for camera \"" - + src.getFrameProvider().getName() - + "\" and loaded " - + usbSrc.configuration.pipelineSettings.size() - + " pipelines"; - }); + () -> + "Matched config for camera \"" + + src.getFrameProvider().getName() + + "\" and loaded " + + usbSrc.configuration.pipelineSettings.size() + + " pipelines"); } logger.info("Adding " + collectedSources.size() + " configs to VMM."); diff --git a/photon-server/src/main/java/org/photonvision/common/configuration/CameraConfiguration.java b/photon-server/src/main/java/org/photonvision/common/configuration/CameraConfiguration.java index d71030483..63b8eaabb 100644 --- a/photon-server/src/main/java/org/photonvision/common/configuration/CameraConfiguration.java +++ b/photon-server/src/main/java/org/photonvision/common/configuration/CameraConfiguration.java @@ -33,12 +33,20 @@ import org.photonvision.vision.processes.PipelineManager; public class CameraConfiguration { private static final Logger logger = new Logger(CameraConfiguration.class, LogGroup.Camera); + /** Name as reported by CSCore */ public String baseName = ""; + + /** Name used to title the subfolder of this config */ public String uniqueName = ""; + + /** User-set nickname */ public String nickname = ""; - public double FOV = 70; + + /** Can be either path (ex /dev/videoX) or index (ex 1). */ public String path = ""; + public CameraType cameraType = CameraType.UsbCamera; + public double FOV = 70; public CameraCalibrationCoefficients calibration; public List cameraLeds = new ArrayList<>(); public int currentPipelineIndex = -1; diff --git a/photon-server/src/main/java/org/photonvision/common/configuration/ConfigManager.java b/photon-server/src/main/java/org/photonvision/common/configuration/ConfigManager.java index 8abcc06e3..a5ced6ab2 100644 --- a/photon-server/src/main/java/org/photonvision/common/configuration/ConfigManager.java +++ b/photon-server/src/main/java/org/photonvision/common/configuration/ConfigManager.java @@ -22,10 +22,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.stream.Collectors; import org.photonvision.common.logging.LogGroup; import org.photonvision.common.logging.Logger; @@ -172,7 +169,8 @@ public class ConfigManager { // Delete old pipe configs so that we don't get any conflicts try { var pipelineFolder = Path.of(subdir.toString(), "pipelines"); - Files.list(pipelineFolder).forEach(it -> it.toFile().delete()); + if (pipelineFolder.toFile().exists()) + Files.list(pipelineFolder).map(Path::toFile).filter(File::exists).forEach(File::delete); } catch (IOException e) { logger.error("Exception while deleting old configs!"); e.printStackTrace(); @@ -240,26 +238,31 @@ public class ConfigManager { // Load pipelines by mapping the files within the pipelines subdir // to their deserialized equivalents + var pipelineSubdirectory = Path.of(subdir.toString(), "pipelines"); List settings = - Files.list(Path.of(subdir.toString(), "pipelines")) - .filter(p -> p.toFile().isFile()) - .map( - p -> { - var relativizedFilePath = - getRootFolder().toAbsolutePath().relativize(p).toString(); - try { - return JacksonUtils.deserialize(p, CVPipelineSettings.class); - } catch (JsonProcessingException e) { - logger.error("Exception while deserializing " + relativizedFilePath); - e.printStackTrace(); - } catch (IOException e) { - logger.warn( - "Could not load pipeline at " + relativizedFilePath + "! Skipping..."); - } - return null; - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + pipelineSubdirectory.toFile().exists() + ? Files.list(pipelineSubdirectory) + .filter(p -> p.toFile().isFile()) + .map( + p -> { + var relativizedFilePath = + getRootFolder().toAbsolutePath().relativize(p).toString(); + try { + return JacksonUtils.deserialize(p, CVPipelineSettings.class); + } catch (JsonProcessingException e) { + logger.error("Exception while deserializing " + relativizedFilePath); + e.printStackTrace(); + } catch (IOException e) { + logger.warn( + "Could not load pipeline at " + + relativizedFilePath + + "! Skipping..."); + } + return null; + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + : Collections.emptyList(); loadedConfig.driveModeSettings = driverMode; loadedConfig.addPipelineSettings(settings); diff --git a/photon-server/src/main/java/org/photonvision/server/Main.java b/photon-server/src/main/java/org/photonvision/server/Main.java deleted file mode 100644 index 3c9fb78db..000000000 --- a/photon-server/src/main/java/org/photonvision/server/Main.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (C) 2020 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.server; - -public class Main { - - public static void main(String[] args) { - Server.main(5800); - } -} diff --git a/photon-server/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java b/photon-server/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java index 868766c06..6acf36024 100644 --- a/photon-server/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java +++ b/photon-server/src/main/java/org/photonvision/vision/processes/VisionSourceManager.java @@ -37,89 +37,143 @@ public class VisionSourceManager { private static final Logger logger = new Logger(VisionSourceManager.class, LogGroup.Camera); - public static List loadAllSources( - Collection camerasConfiguration) { + /** + * Load vision sources based on currently connected hardware. + * + * @param loadedConfigs The {@link CameraConfiguration}s loaded from disk. + */ + public static List loadAllSources(Collection loadedConfigs) { + logger.trace(() -> "Loading all sources..."); + List usbCamInfos = Arrays.asList(UsbCamera.enumerateUsbCameras()); + logger.trace(() -> "CSCore detected " + usbCamInfos.size() + " USB cameras!"); for (var usbCamInfo : usbCamInfos) { logger.info( "Adding local video device - \"" + usbCamInfo.name + "\" at \"" + usbCamInfo.path + "\""); } - return LoadAllSources(camerasConfiguration, usbCamInfos); + return LoadAllSources(loadedConfigs, usbCamInfos); } + /** + * Load vision sources based on currently connected hardware and preexisting configs. + * + * @param loadedConfigs The configs loaded from disk. + * @param detectedCamInfos The {@link UsbCameraInfo}s detected by {@link + * UsbCamera#enumerateUsbCameras()}. + */ public static List LoadAllSources( - Collection camerasConfiguration, List usbCameraInfos) { - var UsbCamerasConfiguration = - camerasConfiguration.stream() + Collection loadedConfigs, List detectedCamInfos) { + var loadedUsbCamConfigs = + loadedConfigs.stream() .filter(configuration -> configuration.cameraType == CameraType.UsbCamera) .collect(Collectors.toList()); // var HttpCamerasConfiguration = camerasConfiguration.stream().filter(configuration -> // configuration.cameraType == CameraType.HttpCamera); - var matchedCameras = matchUSBCameras(usbCameraInfos, UsbCamerasConfiguration); - return loadUSBCameraSources(matchedCameras); + var matchedCameras = matchUSBCameras(detectedCamInfos, loadedUsbCamConfigs); + + // turn the matched cameras into VisionSources + return loadVisionSourcesFromCamConfigs(matchedCameras); } private static NetworkFrameProvider loadHTTPCamera(CameraConfiguration config) { throw new NotImplementedException(""); } + /** + * 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. + */ private static List matchUSBCameras( - List infos, List cameraConfigurationList) { - ArrayList loopableInfo = new ArrayList<>(infos); + List detectedCamInfos, List loadedUsbCamConfigs) { + ArrayList detectedCameraList = new ArrayList<>(detectedCamInfos); List cameraConfigurations = new ArrayList<>(); - for (CameraConfiguration config : cameraConfigurationList) { + // loop over all the configs loaded from disk + for (CameraConfiguration config : loadedUsbCamConfigs) { UsbCameraInfo cameraInfo; - if (!StringUtils.isNumeric(config.path)) { - // matching by path + + // Load by path vs by index -- if the path is numeric we'll match by index + if (StringUtils.isNumeric(config.path)) { + // match by index + var index = Integer.parseInt(config.path); + logger.trace( + "Trying to find a match for loaded camera " + config.baseName + " with index " + index); cameraInfo = - loopableInfo.stream() - .filter(usbCameraInfo -> usbCameraInfo.path.equals(config.path)) + detectedCameraList.stream() + .filter(usbCameraInfo -> usbCameraInfo.dev == index) .findFirst() .orElse(null); } else { - // match by index + // matching by path + logger.trace( + "Trying to find a match for loaded camera " + + config.baseName + + " with path " + + config.path); cameraInfo = - loopableInfo.stream() - .filter(usbCameraInfo -> usbCameraInfo.dev == Integer.parseInt(config.path)) + detectedCameraList.stream() + .filter(usbCameraInfo -> usbCameraInfo.path.equals(config.path)) .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) { - loopableInfo.remove(cameraInfo); + logger.trace("Matched the config for " + config.baseName + " to a physical camera!"); + detectedCameraList.remove(cameraInfo); cameraConfigurations.add(config); } } - for (UsbCameraInfo info : loopableInfo) { - // create new camera config for all new cameras - String name = info.name.replaceAll("[^\\x00-\\x7F]", ""); - String uniqueName = name; - int suffix = 0; + // If we have any unmatched cameras left, create a new CameraConfiguration for them here. + logger.trace( + "After matching loaded configs " + detectedCameraList.size() + " cameras were unmatched."); + for (UsbCameraInfo info : detectedCameraList) { + // create new camera config for all new cameras + String baseName = + info.name.replaceAll("[^\\x00-\\x7F]", ""); // Remove all non-ASCII characters + String uniqueName = baseName.replaceAll(" ", "_"); // Replace spaces with underscores; + + int suffix = 0; while (containsName(cameraConfigurations, uniqueName)) { suffix++; uniqueName = String.format("%s (%d)", uniqueName, suffix); } + logger.info("Creating a new camera config for camera " + uniqueName); + CameraConfiguration configuration = - new CameraConfiguration(name, uniqueName, uniqueName, info.path); + new CameraConfiguration(baseName, uniqueName, uniqueName, info.path); cameraConfigurations.add(configuration); } + logger.trace("Matched or created " + cameraConfigurations.size() + " camera configs!"); return cameraConfigurations; } - private static List loadUSBCameraSources(List configurations) { + private static List loadVisionSourcesFromCamConfigs( + List camConfigs) { List usbCameraSources = new ArrayList<>(); - configurations.forEach( - configuration -> usbCameraSources.add(new USBCameraSource(configuration))); + camConfigs.forEach(configuration -> usbCameraSources.add(new USBCameraSource(configuration))); return usbCameraSources; } - private static boolean containsName(final List list, final String name) { - return list.stream().anyMatch(configuration -> configuration.uniqueName.equals(name)); + /** + * Check if a given config list contains the given unique name. + * + * @param configList A list of camera configs. + * @param uniqueName The unique name. + * @return If the list of configs contains the unique name. + */ + private static boolean containsName( + final List configList, final String uniqueName) { + return configList.stream() + .anyMatch(configuration -> configuration.uniqueName.equals(uniqueName)); } }