Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified gradlew
100644 → 100755
Empty file.
92 changes: 92 additions & 0 deletions simgui-ds.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
{
"keyboardJoysticks": [
{
"axisConfig": [
{
"decKey": 65,
"incKey": 68
},
{
"decKey": 87,
"incKey": 83
},
{
"decKey": 69,
"decayRate": 0.0,
"incKey": 82,
"keyRate": 0.009999999776482582
}
],
"axisCount": 3,
"buttonCount": 4,
"buttonKeys": [
90,
88,
67,
86
],
"povConfig": [
{
"key0": 328,
"key135": 323,
"key180": 322,
"key225": 321,
"key270": 324,
"key315": 327,
"key45": 329,
"key90": 326
}
],
"povCount": 1
},
{
"axisConfig": [
{
"decKey": 74,
"incKey": 76
},
{
"decKey": 73,
"incKey": 75
}
],
"axisCount": 2,
"buttonCount": 4,
"buttonKeys": [
77,
44,
46,
47
],
"povCount": 0
},
{
"axisConfig": [
{
"decKey": 263,
"incKey": 262
},
{
"decKey": 265,
"incKey": 264
}
],
"axisCount": 2,
"buttonCount": 6,
"buttonKeys": [
260,
268,
266,
261,
269,
267
],
"povCount": 0
},
{
"axisCount": 0,
"buttonCount": 0,
"povCount": 0
}
]
}
14 changes: 7 additions & 7 deletions src/main/java/frc/robot/BuildConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ public final class BuildConstants {
public static final String MAVEN_GROUP = "";
public static final String MAVEN_NAME = "Rebuilt";
public static final String VERSION = "unspecified";
public static final int GIT_REVISION = -1;
public static final String GIT_SHA = "UNKNOWN";
public static final String GIT_DATE = "UNKNOWN";
public static final String GIT_BRANCH = "UNKNOWN";
public static final String BUILD_DATE = "2026-01-12 00:23:28 EST";
public static final long BUILD_UNIX_TIME = 1768195408487L;
public static final int DIRTY = 129;
public static final int GIT_REVISION = 10;
public static final String GIT_SHA = "1d207d0cc56430ccf1d54165ce584fd9b10e547f";
public static final String GIT_DATE = "2026-01-16 21:44:52 EST";
public static final String GIT_BRANCH = "vision";
public static final String BUILD_DATE = "2026-01-21 21:48:49 EST";
public static final long BUILD_UNIX_TIME = 1769050129489L;
public static final int DIRTY = 1;

private BuildConstants() {}
}
40 changes: 40 additions & 0 deletions src/main/java/frc/robot/RobotContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@

package frc.robot;

import static frc.robot.subsystems.vision.VisionConstants.*;

import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.Commands;
import edu.wpi.first.wpilibj2.command.button.CommandPS5Controller;
import frc.robot.constants.Constants;
import frc.robot.subsystems.drivetrain.CommandSwerveDrivetrain;
import frc.robot.subsystems.vision.Vision;
import frc.robot.subsystems.vision.VisionIO;
import frc.robot.subsystems.vision.VisionIOLimelight;
import frc.robot.subsystems.vision.VisionIOPhotonVisionSim;

public class RobotContainer {

private final Vision vision;

// Controllers
public static CommandPS5Controller driverController = new CommandPS5Controller(0);
public static CommandPS5Controller operatorController = new CommandPS5Controller(1);
Expand All @@ -19,6 +28,37 @@ public class RobotContainer {
public static CommandSwerveDrivetrain drivetrain;

public RobotContainer() {
switch (Constants.currentMode) {
case REAL:
// Real robot, instantiate hardware IO implementations
vision =
new Vision(
drivetrain::addVisionMeasurement,
new VisionIOLimelight(camera0Name, () -> drivetrain.odometryHeading),
new VisionIOLimelight(camera1Name, () -> drivetrain.odometryHeading));

break;

case SIM:
// Sim robot, instantiate physics sim IO implementations
vision =
new Vision(
drivetrain::addVisionMeasurement,
new VisionIOPhotonVisionSim("camera0Name", robotToCamera0, drivetrain::getRobotPose),
new VisionIOPhotonVisionSim("camera1Name", robotToCamera1, drivetrain::getRobotPose));
break;

default:
// Replayed robot, disable IO implementations
// (Use same number of dummy implementations as the real robot)
vision =
new Vision(
drivetrain::addVisionMeasurement,
new VisionIO() {},
new VisionIO() {});
break;
}

configureBindings();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import edu.wpi.first.math.controller.PIDController;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.interpolation.TimeInterpolatableBuffer;
import edu.wpi.first.math.kinematics.ChassisSpeeds;
import edu.wpi.first.math.numbers.N1;
import edu.wpi.first.math.numbers.N3;
Expand All @@ -39,11 +40,13 @@
import frc.robot.util.PoseUtils;
import frc.robot.util.TunerConstants;
import frc.robot.util.TunerConstants.TunerSwerveDrivetrain;
import java.util.Optional;
import java.util.function.Supplier;
import org.littletonrobotics.junction.AutoLogOutput;
import org.littletonrobotics.junction.Logger;

public class CommandSwerveDrivetrain extends TunerSwerveDrivetrain implements Subsystem {
public TimeInterpolatableBuffer<Pose2d> poseBuffer = TimeInterpolatableBuffer.createBuffer(3);

private static final double kSimLoopPeriod = 0.002; // 2 ms
private Notifier m_simNotifier = null;
Expand Down Expand Up @@ -389,6 +392,14 @@ public Command alignToAngleFieldRelativeCommand(Rotation2d angle, boolean lockDr
.until(() -> isRobotAtAngleSetPoint));
}

public ChassisSpeeds getCurrentSpeeds() {
return this.getState().Speeds;
}

public Optional<Pose2d> getPoseAtTime(double time) {
return poseBuffer.getSample(time);
}

@Override
public void periodic() {

Expand Down
179 changes: 179 additions & 0 deletions src/main/java/frc/robot/subsystems/vision/Vision.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package frc.robot.subsystems.vision;
// Copyright (c) 2021-2026 Littleton Robotics
// http://github.com/Mechanical-Advantage
//
// Use of this source code is governed by a BSD
// license that can be found in the LICENSE file
// at the root directory of this project.

import static frc.robot.subsystems.vision.VisionConstants.*;

import edu.wpi.first.math.Matrix;
import edu.wpi.first.math.VecBuilder;
import edu.wpi.first.math.geometry.Pose2d;
import edu.wpi.first.math.geometry.Pose3d;
import edu.wpi.first.math.geometry.Rotation2d;
import edu.wpi.first.math.numbers.N1;
import edu.wpi.first.math.numbers.N3;
import edu.wpi.first.wpilibj.Alert;
import edu.wpi.first.wpilibj.Alert.AlertType;
import edu.wpi.first.wpilibj2.command.SubsystemBase;
import frc.robot.subsystems.vision.VisionIO.PoseObservationType;
import java.util.LinkedList;
import java.util.List;
import org.littletonrobotics.junction.Logger;

public class Vision extends SubsystemBase {
private final VisionConsumer consumer; //lamda expression that takes in values and records a vision measurement
private final VisionIO[] io;
private final VisionIOInputsAutoLogged[] inputs;
private final Alert[] disconnectedAlerts;

public Vision(VisionConsumer consumer, VisionIO... io) {
this.consumer = consumer;
this.io = io;

//for loops serve the purpose of intitializing inputs for all cameras, as the ios are stored in an array
// Initialize inputs
this.inputs = new VisionIOInputsAutoLogged[io.length];
for (int i = 0; i < inputs.length; i++) {
inputs[i] = new VisionIOInputsAutoLogged();
}

// Initialize disconnected alerts
this.disconnectedAlerts = new Alert[io.length];
for (int i = 0; i < inputs.length; i++) {
disconnectedAlerts[i] =
new Alert(
"Vision camera " + Integer.toString(i) + " is disconnected.", AlertType.kWarning);
}
}

/**
* Returns the X angle to the best target, which can be used for simple servoing with vision.
*
* @param cameraIndex The index of the camera to use.
*/
public Rotation2d getTargetX(int cameraIndex) {
return inputs[cameraIndex].latestTargetObservation.tx();
}

@Override
public void periodic() {
for (int i = 0; i < io.length; i++) {
io[i].updateInputs(inputs[i]);
Logger.processInputs("Vision/Camera" + Integer.toString(i), inputs[i]);
}

// Initialize logging values
List<Pose3d> allTagPoses = new LinkedList<>();
List<Pose3d> allRobotPoses = new LinkedList<>();
List<Pose3d> allRobotPosesAccepted = new LinkedList<>();
List<Pose3d> allRobotPosesRejected = new LinkedList<>();

// Loop over cameras
for (int cameraIndex = 0; cameraIndex < io.length; cameraIndex++) {
// Update disconnected alert
disconnectedAlerts[cameraIndex].set(!inputs[cameraIndex].connected);

// Initialize logging values
List<Pose3d> tagPoses = new LinkedList<>();
List<Pose3d> robotPoses = new LinkedList<>();
List<Pose3d> robotPosesAccepted = new LinkedList<>();
List<Pose3d> robotPosesRejected = new LinkedList<>();

// Add tag poses
for (int tagId : inputs[cameraIndex].tagIds) {
var tagPose = aprilTagLayout.getTagPose(tagId);
if (tagPose.isPresent()) {
tagPoses.add(tagPose.get());
}
}

// Loop over pose observations
for (var observation : inputs[cameraIndex].poseObservations) {
// Check whether to reject pose
boolean rejectPose =
observation.tagCount() == 0 // Must have at least one tag
|| (observation.tagCount() == 1
//ambiguity is 0 for megatag2
&& observation.ambiguity() > maxAmbiguity) // Cannot be high ambiguity
|| Math.abs(observation.pose().getZ())
> maxZError // Must have realistic Z coordinate

// Must be within the field boundaries
|| observation.pose().getX() < 0.0
|| observation.pose().getX() > aprilTagLayout.getFieldLength()
|| observation.pose().getY() < 0.0
|| observation.pose().getY() > aprilTagLayout.getFieldWidth();

// Add pose to log
robotPoses.add(observation.pose());
if (rejectPose) {
robotPosesRejected.add(observation.pose());
} else {
robotPosesAccepted.add(observation.pose());
}

// Skip if rejected
if (rejectPose) {
continue;
}

// Calculate standard deviations
double stdDevFactor =
Math.pow(observation.averageTagDistance(), 2.0) / observation.tagCount();
double linearStdDev = linearStdDevBaseline * stdDevFactor;
double angularStdDev = angularStdDevBaseline * stdDevFactor;
if (observation.type() == PoseObservationType.MEGATAG_2) {
linearStdDev *= linearStdDevMegatag2Factor;
angularStdDev *= angularStdDevMegatag2Factor;
}
if (cameraIndex < cameraStdDevFactors.length) {
linearStdDev *= cameraStdDevFactors[cameraIndex];
angularStdDev *= cameraStdDevFactors[cameraIndex];
}

// Send vision observation
consumer.accept(
observation.pose().toPose2d(),
observation.timestamp(),
VecBuilder.fill(linearStdDev, linearStdDev, angularStdDev));
}

// Log camera metadata
Logger.recordOutput(
"Vision/Camera" + Integer.toString(cameraIndex) + "/TagPoses",
tagPoses.toArray(new Pose3d[0]));
Logger.recordOutput(
"Vision/Camera" + Integer.toString(cameraIndex) + "/RobotPoses",
robotPoses.toArray(new Pose3d[0]));
Logger.recordOutput(
"Vision/Camera" + Integer.toString(cameraIndex) + "/RobotPosesAccepted",
robotPosesAccepted.toArray(new Pose3d[0]));
Logger.recordOutput(
"Vision/Camera" + Integer.toString(cameraIndex) + "/RobotPosesRejected",
robotPosesRejected.toArray(new Pose3d[0]));
allTagPoses.addAll(tagPoses);
allRobotPoses.addAll(robotPoses);
allRobotPosesAccepted.addAll(robotPosesAccepted);
allRobotPosesRejected.addAll(robotPosesRejected);
}

// Log summary data
Logger.recordOutput("Vision/Summary/TagPoses", allTagPoses.toArray(new Pose3d[0]));
Logger.recordOutput("Vision/Summary/RobotPoses", allRobotPoses.toArray(new Pose3d[0]));
Logger.recordOutput(
"Vision/Summary/RobotPosesAccepted", allRobotPosesAccepted.toArray(new Pose3d[0]));
Logger.recordOutput(
"Vision/Summary/RobotPosesRejected", allRobotPosesRejected.toArray(new Pose3d[0]));
}

@FunctionalInterface
public static interface VisionConsumer {
public void accept(
Pose2d visionRobotPoseMeters,
double timestampSeconds,
Matrix<N3, N1> visionMeasurementStdDevs);
}
}
Loading