package frc.robot.subsystems; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Consumer; import org.photonvision.EstimatedRobotPose; import org.photonvision.PhotonCamera; import org.photonvision.PhotonPoseEstimator; import org.photonvision.PhotonPoseEstimator.PoseStrategy; import org.photonvision.targeting.PhotonPipelineResult; import org.photonvision.targeting.PhotonTrackedTarget; import edu.wpi.first.math.geometry.Pose3d; import edu.wpi.first.math.geometry.Transform3d; import edu.wpi.first.wpilibj2.command.SubsystemBase; import frc.robot.constants.CompetitionConstants; import frc.robot.constants.PhotonConstants; import frc.robot.utilities.PhotonVisionConfig; import frc.robot.utilities.VisualPose; /** * This "Subsystem" is not actually a Subsystem. The intent is for this to be treated as * a "resource", that is, something that is not inherently a physical mechanism to be controlled. * * A "resource" in this instance should be thought of as something that can be safely shared * by other Subsystems generally without collision if more that one Subsystem requires the * "resource" at any given time. * * Resources should NOT produce Commands, they should not have a default Command. * Resources do not have behaviors, and because Commands are in of themselves behaviors, * this class should not have Commands. * * Part of the thinking behind creating the PhotonVision components this way is to rely * on the CommandScheduler to call periodic. If this weren't the case, some other subsystem * would have to manage calling for periodic updates, while still sharing the resource with * other subsystems somehow. * * This class is dynamic, by adding or removing PhotonVisionConfig objects to the "configs" * List in the PhotonConstants file, you change what is set up internally in this class. * 1 config means 1 camera, 1 estimator, 1 stored pipeline result, 2 configs means 2 cameras, * 2 estimators, etc. etc. */ public class PhotonVision extends SubsystemBase { private PhotonCamera[] cameras; private PhotonPoseEstimator[] estimators; private List latestResults; private ArrayList> poseEstimateConsumers; public PhotonVision() { cameras = new PhotonCamera[PhotonConstants.configs.size()]; estimators = new PhotonPoseEstimator[PhotonConstants.configs.size()]; latestResults = new ArrayList(); for(int i = 0; i < PhotonConstants.configs.size(); i++) { cameras[i] = new PhotonCamera(PhotonConstants.configs.get(i).cameraName()); estimators[i] = new PhotonPoseEstimator( CompetitionConstants.kTagLayout, PoseStrategy.MULTI_TAG_PNP_ON_COPROCESSOR, PhotonConstants.configs.get(i).robotToCamera() ); latestResults.add(null); } poseEstimateConsumers = new ArrayList>(); } @Override public void periodic() { for(int i = 0; i < cameras.length; i++) { List results = cameras[i].getAllUnreadResults(); if(!results.isEmpty()) { latestResults.set(i, results.get(results.size() - 1)); } Optional pose = estimators[i].update(latestResults.get(i)); if(!pose.isEmpty()) { VisualPose visualPose = new VisualPose( cameras[i].getName(), pose.get().estimatedPose.toPose2d(), pose.get().timestampSeconds ); for(Consumer consumer: poseEstimateConsumers) { consumer.accept(visualPose); } } } } /** * Returns the best 3D pose for a given AprilTag ID as seen by the cameras on the robot. * * All cameras fields of view are observed, if no camera can see the given tag ID, this * method will return Optional.empty(). * * Note that this method has no minimum confidence threshold for a tag. This means that * if one camera thinks it sees the tag, even with very low confidence, it'll still return * some sort of pose. * * @param tagID The ID of the tag to look for in the latest results from all cameras * @return An Optional object containing a Pose3d object, or Optional.empty() if * the tag is not present anywhere in the robots field of view. */ public Optional getBestPoseForTag(int tagID) { PhotonVisionConfig config = null; Transform3d bestCameraToTarget = null; float bestConfidence = -1; for(int cameraIndex = 0; cameraIndex < latestResults.size(); cameraIndex++) { if(latestResults.get(cameraIndex) == null) { continue; } for(PhotonTrackedTarget target: latestResults.get(cameraIndex).getTargets()) { if(target.getFiducialId() != tagID || bestConfidence > target.getDetectedObjectConfidence()) { continue; } config = PhotonConstants.configs.get(cameraIndex); bestCameraToTarget = target.bestCameraToTarget; bestConfidence = target.getDetectedObjectConfidence(); } } if(bestCameraToTarget == null) { return Optional.empty(); } // This is based on what PhotonVision does for multitag Pose estimation // See PhotonPoseEstimator.multiTagOnCoprocStrategy // TODO This doesn't currently account for the offset of the tag relative to say the hub return Optional.of(Pose3d.kZero .plus(bestCameraToTarget.inverse()) .relativeTo(CompetitionConstants.kTagLayout.getOrigin()) .plus(config.robotToCamera().inverse()) .plus(CompetitionConstants.kRobotToShooter)); } /** * Add a Consumer of VisualPose records to the PhotonVision resource. * * Each consumer will receive a VisualPose object when any camera produces a new * VisualPose. * * The number of Poses produced in a given 20ms cycle is the same number as how many * cameras there are on the robot, as currently all cameras configuration will generate * a Pose2d * * @param consumer The lambda or functional reference that will consume Poses produced * by the PhotonVision resource. */ public void addPoseEstimateConsumer(Consumer consumer) { poseEstimateConsumers.add(consumer); } /*public Trigger tagPrescenseTrigger(int targetTag) { return new Trigger(() -> { return List.of(latestResults).stream() .filter((p) -> p != null) .anyMatch((p) -> { return p.getTargets().stream().map(PhotonTrackedTarget::getFiducialId).anyMatch((i) -> { return i == targetTag; }); }); }); }*/ /* @Override public OptionalDouble getTagDistanceFromCameraByID(int id) { if (latestResult == null) { return OptionalDouble.empty(); } if (!latestResult.hasTargets()) { return OptionalDouble.empty(); } Optional desiredTarget = getTargetFromList(latestResult.getTargets(), id); if (desiredTarget.isEmpty()) { return OptionalDouble.empty(); } return OptionalDouble.of( PhotonUtils.calculateDistanceToTargetMeters( cameraHeightMeters, CompetitionConstants.kTagLayout.getTagPose(id).get().getZ(), cameraPitchRadians, Units.degreesToRadians(desiredTarget.get().getPitch())) ); } @Override public OptionalDouble getTagPitchByID(int id) { if(latestResult == null) { OptionalDouble.empty(); } if (!latestResult.hasTargets()) { return OptionalDouble.empty(); } Optional desiredTarget = getTargetFromList(latestResult.getTargets(), id); if (desiredTarget.isEmpty()) { return OptionalDouble.empty(); } return OptionalDouble.of( desiredTarget.get().getPitch() ); } @Override public OptionalDouble getTagYawByID(int id) { if(latestResult == null) { OptionalDouble.empty(); } if (!latestResult.hasTargets()) { return OptionalDouble.empty(); } Optional desiredTarget = getTargetFromList(latestResult.getTargets(), id); if (desiredTarget.isEmpty()) { return OptionalDouble.empty(); } return OptionalDouble.of( desiredTarget.get().getYaw() ); } private Optional getTargetFromList(List targets, int id) { for (PhotonTrackedTarget target : targets) { if (target.getFiducialId() == id) { return Optional.of(target); } } return Optional.empty(); } @Override public int[] getVisibleTagIDs() { if(latestResult == null) { return new int[] {}; } return latestResult.getTargets().stream().mapToInt(PhotonTrackedTarget::getFiducialId).toArray(); } */ }