001/************************ PROJECT ZIXI ************************/ 002/* Copyright (c) 2026 StuyPulse Robotics. All rights reserved.*/ 003/* This work is licensed under the terms of the MIT license. */ 004/**************************************************************/ 005package com.stuypulse.robot.subsystems.vision; 006 007import com.stuypulse.robot.constants.FieldConstants; 008import com.stuypulse.robot.constants.GlobalSettings; 009import com.stuypulse.robot.subsystems.vision.VisionConstants.Camera; 010import com.stuypulse.robot.subsystems.vision.VisionConstants.CameraData; 011import com.stuypulse.robot.subsystems.vision.VisionConstants.Pipeline; 012import com.stuypulse.robot.subsystems.vision.VisionConstants.VisionSettings; 013import com.stuypulse.robot.subsystems.vision.VisionIO.MegaTagMode; 014import com.stuypulse.robot.subsystems.vision.VisionIO.PoseObservation; 015import com.stuypulse.robot.subsystems.vision.VisionIO.PoseObservationType; 016import com.stuypulse.robot.subsystems.vision.VisionIO.VisionIOOutputs; 017import com.stuypulse.robot.util.PeriodicSubsystem; 018 019import org.wpilib.command3.Command; 020import org.wpilib.math.geometry.Pose2d; 021import org.wpilib.math.geometry.Pose3d; 022import org.wpilib.math.geometry.Rotation2d; 023import org.wpilib.math.linalg.Matrix; 024import org.wpilib.math.linalg.VecBuilder; 025import org.wpilib.math.numbers.N1; 026import org.wpilib.math.numbers.N3; 027import org.wpilib.system.Timer; 028import org.wpilib.util.Alert; 029 030import java.util.EnumMap; 031import java.util.Iterator; 032import java.util.LinkedList; 033import java.util.List; 034import java.util.Map.Entry; 035import java.util.Optional; 036import org.littletonrobotics.junction.Logger; 037 038public class Vision extends PeriodicSubsystem { 039 private final VisionConsumer consumer; 040 private final EnumMap<Camera, VisionIO> io; 041 private final EnumMap<Camera, VisionIOInputsAutoLogged> inputs; 042 private final EnumMap<Camera, VisionIOOutputs> outputs; 043 private final EnumMap<Camera, Alert> disconnectedAlerts; 044 045 private final Timer hdrTimer; 046 047 public Vision(VisionConsumer consumer, EnumMap<Camera, VisionIO> cameraIOMap) { 048 this.consumer = consumer; 049 050 this.io = new EnumMap<>(cameraIOMap); 051 052 // Initialize inputs 053 this.inputs = new EnumMap<>(Camera.class); 054 this.outputs = new EnumMap<>(Camera.class); 055 056 // Initialize disconnected alerts 057 this.disconnectedAlerts = new EnumMap<>(Camera.class); 058 059 this.hdrTimer = new Timer(); 060 for (Camera camera : io.keySet()) { 061 inputs.put(camera, new VisionIOInputsAutoLogged()); 062 outputs.put(camera, new VisionIOOutputs()); 063 disconnectedAlerts.put( 064 camera, 065 new Alert( 066 "Vision " + camera.getName(), 067 "Vision camera " + camera.getName() + " is disconnected.", 068 Alert.Level.MEDIUM)); 069 } 070 071 hdrTimer.start(); 072 } 073 074 /** 075 * Returns the X angle to the best target, which can be used for simple servoing with vision. 076 * 077 * @param camera The camera to use 078 */ 079 public Rotation2d getTargetX(Camera camera) { 080 return inputs.get(camera).latestTargetObservation.tx(); 081 } 082 083 public Optional<Integer> getPipeline() { 084 Iterator<VisionIOInputsAutoLogged> iterator = inputs.values().iterator(); 085 return iterator.hasNext() ? Optional.of(iterator.next().currentPipeline) : Optional.empty(); 086 } 087 088 @Override 089 public void periodic() { 090 if (!GlobalSettings.EnabledSubsystems.VISION.get()) { 091 return; 092 } 093 094 for (Entry<Camera, VisionIO> entry : io.entrySet()) { 095 VisionIO currentIO = entry.getValue(); 096 VisionIOInputsAutoLogged currentInputs = inputs.get(entry.getKey()); 097 currentIO.updateInputs(currentInputs); 098 Logger.processInputs("Vision/" + entry.getKey().getName(), currentInputs); 099 } 100 101 Optional<Integer> pipeline = getPipeline(); 102 103 if (hdrTimer.hasElapsed(VisionConstants.VisionSettings.HDR_SWITCHING_INTERVAL) 104 && VisionConstants.VisionSettings.HDR_ENABLED.get() 105 && pipeline.isPresent()) { 106 int nextPipeline = 107 pipeline.get() == Pipeline.HIGH_SUN.getIndex() 108 ? Pipeline.LOW_SUN.getIndex() 109 : Pipeline.HIGH_SUN.getIndex(); 110 111 for (VisionIOOutputs outputs : outputs.values()) { 112 outputs.pipeline = nextPipeline; 113 } 114 115 hdrTimer.reset(); 116 } 117 118 // Initialize logging values 119 List<Pose3d> allTagPoses = new LinkedList<>(); 120 List<Pose3d> allRobotPoses = new LinkedList<>(); 121 List<Pose3d> allRobotPosesAccepted = new LinkedList<>(); 122 List<Pose3d> allRobotPosesRejected = new LinkedList<>(); 123 124 // Loop over cameras 125 for (Entry<Camera, VisionIO> entry : io.entrySet()) { 126 CameraData currentCameraData = entry.getKey().getData(); 127 VisionIOInputsAutoLogged currentInputs = inputs.get(entry.getKey()); 128 129 // Update disconnected alert 130 disconnectedAlerts.get(entry.getKey()).set(!currentInputs.connected); 131 132 // Initialize logging values 133 List<Pose3d> tagPoses = new LinkedList<>(); 134 List<Pose3d> robotPoses = new LinkedList<>(); 135 List<Pose3d> robotPosesAccepted = new LinkedList<>(); 136 List<Pose3d> robotPosesRejected = new LinkedList<>(); 137 138 // Add tag poses 139 for (int tagId : currentInputs.tagIds) { 140 Optional<Pose3d> tagPose = FieldConstants.FIELD_LAYOUT.getTagPose(tagId); 141 if (tagPose.isPresent()) { 142 tagPoses.add(tagPose.get()); 143 } 144 } 145 146 // Loop over pose observations 147 for (PoseObservation observation : currentInputs.poseObservations) { 148 // Check whether to reject pose 149 boolean rejectPose = 150 observation.tagCount() == 0 // Must have at least one tag 151 || (observation.tagCount() == 1 152 && observation.ambiguity() 153 > VisionSettings 154 .MAX_AMBIGUITY) // Cannot be high ambiguity 155 || Math.abs(observation.pose().getZ()) 156 > VisionSettings 157 .MAX_Z_ERROR // Must have realistic Z coordinate 158 159 // Must be within the field boundaries 160 || observation.pose().getX() < 0.0 161 || observation.pose().getX() 162 > FieldConstants.FIELD_LAYOUT.getFieldLength() 163 || observation.pose().getY() < 0.0 164 || observation.pose().getY() 165 > FieldConstants.FIELD_LAYOUT.getFieldWidth(); 166 167 // Add pose to log 168 robotPoses.add(observation.pose()); 169 if (rejectPose) { 170 robotPosesRejected.add(observation.pose()); 171 } else { 172 robotPosesAccepted.add(observation.pose()); 173 } 174 175 // Skip if rejected 176 if (rejectPose) { 177 continue; 178 } 179 180 // Calculate standard deviations 181 double stdDevFactor = 182 Math.pow(observation.averageTagDistance(), 2.0) / observation.tagCount(); 183 double linearStdDev = VisionSettings.LINEAR_STD_DEV_BASELINE * stdDevFactor; 184 double angularStdDev = VisionSettings.ANGULAR_STD_DEV_BASELINE * stdDevFactor; 185 if (observation.type() == PoseObservationType.MEGATAG_2) { 186 linearStdDev *= VisionSettings.LINEAR_STD_DEV_MEGATAG_2_FACTOR; 187 angularStdDev *= VisionSettings.ANGULAR_STD_DEV_MEGATAG_2_FACTOR; 188 } 189 linearStdDev *= currentCameraData.stdDevFactor(); 190 angularStdDev *= currentCameraData.stdDevFactor(); 191 192 // Send vision observation 193 consumer.accept( 194 observation.pose().toPose2d(), 195 observation.timestamp(), 196 VecBuilder.fill(linearStdDev, linearStdDev, angularStdDev)); 197 } 198 199 // Log camera metadata 200 Logger.recordOutput( 201 "Vision/Camera" + currentCameraData.name() + "/TagPoses", 202 tagPoses.toArray(new Pose3d[tagPoses.size()])); 203 Logger.recordOutput( 204 "Vision/Camera" + currentCameraData.name() + "/RobotPoses", 205 robotPoses.toArray(new Pose3d[robotPoses.size()])); 206 Logger.recordOutput( 207 "Vision/Camera" + currentCameraData.name() + "/RobotPosesAccepted", 208 robotPosesAccepted.toArray(new Pose3d[robotPosesAccepted.size()])); 209 Logger.recordOutput( 210 "Vision/Camera" + currentCameraData.name() + "/RobotPosesRejected", 211 robotPosesRejected.toArray(new Pose3d[robotPosesRejected.size()])); 212 allTagPoses.addAll(tagPoses); 213 allRobotPoses.addAll(robotPoses); 214 allRobotPosesAccepted.addAll(robotPosesAccepted); 215 allRobotPosesRejected.addAll(robotPosesRejected); 216 } 217 218 // Log summary data 219 Logger.recordOutput("Vision/Summary/TagPoses", allTagPoses.toArray(new Pose3d[0])); 220 Logger.recordOutput("Vision/Summary/RobotPoses", allRobotPoses.toArray(new Pose3d[0])); 221 Logger.recordOutput( 222 "Vision/Summary/RobotPosesAccepted", allRobotPosesAccepted.toArray(new Pose3d[0])); 223 Logger.recordOutput( 224 "Vision/Summary/RobotPosesRejected", allRobotPosesRejected.toArray(new Pose3d[0])); 225 } 226 227 @Override 228 public void periodicAfterScheduler() { 229 for (Entry<Camera, VisionIO> entry : io.entrySet()) { 230 entry.getValue().applyOutputs(outputs.get(entry.getKey())); 231 } 232 } 233 234 @FunctionalInterface 235 public static interface VisionConsumer { 236 public void accept( 237 Pose2d visionRobotPoseMeters, 238 double timestampSeconds, 239 Matrix<N3, N1> visionMeasurementStdDevs); 240 } 241 242 public void enable() { 243 GlobalSettings.EnabledSubsystems.VISION.set(true); 244 } 245 246 public void disable() { 247 GlobalSettings.EnabledSubsystems.VISION.set(false); 248 } 249 250 public Command setPipeline(int pipeline) { 251 return run(coroutine -> { 252 for (VisionIOOutputs output : outputs.values()) { 253 output.pipeline = pipeline; 254 } 255 }) 256 .named("Vision Set Pipeline"); 257 } 258 259 public Command setMegaTagMode(MegaTagMode mode) { 260 return run(coroutine -> { 261 for (VisionIOOutputs output : outputs.values()) { 262 output.megaTagMode = mode; 263 } 264 }) 265 .named("Vision Set MegaTag Mode"); 266 } 267 268 public Command setVisionEnabled() { 269 return run(coroutine -> { 270 this.enable(); 271 }) 272 .named("Vision Set Enabled"); 273 } 274 275 public Command setVisionDisabled() { 276 return run(coroutine -> { 277 this.disable(); 278 }) 279 .named("Vision Set Enabled"); 280 } 281 282 public Command setAprilTagWhitelist(int[] whitelist) { 283 return run(coroutine -> { 284 for (VisionIOOutputs output : outputs.values()) { 285 output.aprilTagWhitelist = whitelist.clone(); 286 } 287 }) 288 .named("Vision Set April Tag Whitelist"); 289 } 290 291 private Command setIMUMode(int imuMode, String name) { 292 return run(coroutine -> { 293 for (VisionIOOutputs output : outputs.values()) { 294 output.imuMode = imuMode; 295 } 296 }) 297 .named(name); 298 } 299 300 public Command setIMUMode(int imuMode) { 301 return setIMUMode(imuMode, "Vision Set IMU Mode"); 302 } 303 304 public Command resetIMU() { 305 return setIMUMode(VisionSettings.RESET_IMU_INDEX, "Vision Reset IMU"); 306 } 307}