package swervelib.math; import edu.wpi.first.math.MathUtil; import edu.wpi.first.math.Matrix; import edu.wpi.first.math.Nat; import edu.wpi.first.math.VecBuilder; import edu.wpi.first.math.estimator.SwerveDrivePoseEstimator; import edu.wpi.first.math.geometry.Pose2d; import edu.wpi.first.math.geometry.Rotation2d; import edu.wpi.first.math.geometry.Twist2d; import edu.wpi.first.math.interpolation.Interpolatable; import edu.wpi.first.math.interpolation.TimeInterpolatableBuffer; import edu.wpi.first.math.kinematics.SwerveDriveKinematics; import edu.wpi.first.math.kinematics.SwerveModulePosition; import edu.wpi.first.math.numbers.N1; import edu.wpi.first.math.numbers.N3; import edu.wpi.first.util.WPIUtilJNI; import java.util.Arrays; import java.util.Map; import java.util.Objects; /** * Clone of {@link SwerveDrivePoseEstimator} which takes into account gyroscope pitch and roll to achieve a more * accurate estimation, originally made by Team 1466. */ public class SwervePoseEstimator2 // extends SwerveDrivePoseEstimator { /** * Swerve drive kinematics. */ private final SwerveDriveKinematics m_kinematics; /** * Enhanced swerve drive odometry. */ private final SwerveDriveOdometry2 m_odometry; /** * Matrix quotient. */ private final Matrix m_q = new Matrix<>(Nat.N3(), Nat.N1()); /** * Number of swerve modules. */ private final int m_numModules; /** * Interpolation buffer. */ private final TimeInterpolatableBuffer m_poseBuffer = TimeInterpolatableBuffer.createBuffer(1.5); /** * Vision standard deviations. */ private final Matrix m_visionK = new Matrix<>(Nat.N3(), Nat.N3()); /** * Constructs a SwerveDrivePoseEstimator with default standard deviations for the model and vision measurements. * *

The default standard deviations of the model states are 0.1 meters for x, 0.1 meters for y, * and 0.1 radians for heading. The default standard deviations of the vision measurements are 0.9 meters for x, 0.9 * meters for y, and 0.9 radians for heading. * * @param kinematics A correctly-configured kinematics object for your drivetrain. * @param gyroAngle The current gyro angle. * @param modulePositions The current distance measurements and rotations of the swerve modules. * @param initialPoseMeters The starting pose estimate. */ public SwervePoseEstimator2( SwerveDriveKinematics kinematics, Rotation2d gyroAngle, SwerveModulePosition[] modulePositions, Pose2d initialPoseMeters) { this( kinematics, gyroAngle, modulePositions, initialPoseMeters, VecBuilder.fill(0.1, 0.1, 0.1), VecBuilder.fill(0.9, 0.9, 0.9)); } /** * Constructs a SwerveDrivePoseEstimator. * * @param kinematics A correctly-configured kinematics object for your drivetrain. * @param gyroAngle The current gyro angle. * @param modulePositions The current distance and rotation measurements of the swerve modules. * @param initialPoseMeters The starting pose estimate. * @param stateStdDevs Standard deviations of the pose estimate (x position in meters, y position in * meters, and heading in radians). Increase these numbers to trust your state * estimate less. * @param visionMeasurementStdDevs Standard deviations of the vision pose measurement (x position in meters, y * position in meters, and heading in radians). Increase these numbers to trust the * vision pose measurement less. */ public SwervePoseEstimator2( SwerveDriveKinematics kinematics, Rotation2d gyroAngle, SwerveModulePosition[] modulePositions, Pose2d initialPoseMeters, Matrix stateStdDevs, Matrix visionMeasurementStdDevs) { // super(kinematics, gyroAngle, modulePositions, initialPoseMeters); m_kinematics = kinematics; m_odometry = new SwerveDriveOdometry2(kinematics, gyroAngle, modulePositions, initialPoseMeters); for (int i = 0; i < 3; ++i) { m_q.set(i, 0, stateStdDevs.get(i, 0) * stateStdDevs.get(i, 0)); } m_numModules = modulePositions.length; setVisionMeasurementStdDevs(visionMeasurementStdDevs); } /** * Sets the pose estimator's trust of global measurements. This might be used to change trust in vision measurements * after the autonomous period, or to change trust as distance to a vision target increases. * * @param visionMeasurementStdDevs Standard deviations of the vision measurements. Increase these numbers to trust * global measurements from vision less. This matrix is in the form [x, y, theta]ᵀ, * with units in meters and radians. */ public void setVisionMeasurementStdDevs(Matrix visionMeasurementStdDevs) { // super.setVisionMeasurementStdDevs(visionMeasurementStdDevs); var r = new double[3]; for (int i = 0; i < 3; ++i) { r[i] = visionMeasurementStdDevs.get(i, 0) * visionMeasurementStdDevs.get(i, 0); } // Solve for closed form Kalman gain for continuous Kalman filter with A = 0 // and C = I. See wpimath/algorithms.md. for (int row = 0; row < 3; ++row) { if (m_q.get(row, 0) == 0.0) { m_visionK.set(row, row, 0.0); } else { m_visionK.set( row, row, m_q.get(row, 0) / (m_q.get(row, 0) + Math.sqrt(m_q.get(row, 0) * r[row]))); } } } /** * Resets the robot's position on the field. * *

The gyroscope angle does not need to be reset in the user's robot code. The library * automatically takes care of offsetting the gyro angle. * * @param gyroAngle The angle reported by the gyroscope. * @param modulePositions The current distance measurements and rotations of the swerve modules. * @param poseMeters The position on the field that your robot is at. */ public void resetPosition( Rotation2d gyroAngle, SwerveModulePosition[] modulePositions, Pose2d poseMeters) { // super.resetPosition(gyroAngle, modulePositions, poseMeters); // Reset state estimate and error covariance m_odometry.resetPosition(gyroAngle, modulePositions, poseMeters); m_poseBuffer.clear(); } /** * Gets the estimated robot pose. * * @return The estimated robot pose in meters. */ public Pose2d getEstimatedPosition() { return m_odometry.getPoseMeters(); } /** * Adds a vision measurement to the Kalman Filter. This will correct the odometry pose estimate while still accounting * for measurement noise. * *

This method can be called as infrequently as you want, as long as you are calling {@link * SwerveDrivePoseEstimator#update} every loop. * *

To promote stability of the pose estimate and make it robust to bad vision data, we * recommend only adding vision measurements that are already within one meter or so of the current pose estimate. * * @param visionRobotPoseMeters The pose of the robot as measured by the vision camera. * @param timestampSeconds The timestamp of the vision measurement in seconds. Note that if you don't use your * own time source by calling * {@link SwerveDrivePoseEstimator#updateWithTime(double, Rotation2d, * SwerveModulePosition[])} then you must use a timestamp with an epoch since FPGA * startup (i.e., the epoch of this timestamp is the same epoch as * {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()}.) This means that you should * use {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()} as your time source or sync * the epochs. */ public void addVisionMeasurement(Pose2d visionRobotPoseMeters, double timestampSeconds) { // super.addVisionMeasurement(visionRobotPoseMeters, timestampSeconds); // Step 1: Get the pose odometry measured at the moment the vision measurement was made. var sample = m_poseBuffer.getSample(timestampSeconds); if (sample.isEmpty()) { return; } // Step 2: Measure the twist between the odometry pose and the vision pose. var twist = sample.get().poseMeters.log(visionRobotPoseMeters); // Step 3: We should not trust the twist entirely, so instead we scale this twist by a Kalman // gain matrix representing how much we trust vision measurements compared to our current pose. var k_times_twist = m_visionK.times(VecBuilder.fill(twist.dx, twist.dy, twist.dtheta)); // Step 4: Convert back to Twist2d. var scaledTwist = new Twist2d(k_times_twist.get(0, 0), k_times_twist.get(1, 0), k_times_twist.get(2, 0)); // Step 5: Reset Odometry to state at sample with vision adjustment. m_odometry.resetPosition( sample.get().gyroAngle, sample.get().modulePositions, sample.get().poseMeters.exp(scaledTwist)); // Step 6: Replay odometry inputs between sample time and latest recorded sample to update the // pose buffer and correct odometry. for (Map.Entry entry : m_poseBuffer.getInternalBuffer().tailMap(timestampSeconds).entrySet()) { updateWithTime( entry.getKey(), entry.getValue().gyroAngle, entry.getValue().gyroPitch, entry.getValue().gyroRoll, entry.getValue().modulePositions); } } /** * Adds a vision measurement to the Kalman Filter. This will correct the odometry pose estimate while still accounting * for measurement noise. * *

This method can be called as infrequently as you want, as long as you are calling {@link * SwerveDrivePoseEstimator#update} every loop. * *

To promote stability of the pose estimate and make it robust to bad vision data, we * recommend only adding vision measurements that are already within one meter or so of the current pose estimate. * *

Note that the vision measurement standard deviations passed into this method will continue * to apply to future measurements until a subsequent call to * {@link SwerveDrivePoseEstimator#setVisionMeasurementStdDevs(Matrix)} or this method. * * @param visionRobotPoseMeters The pose of the robot as measured by the vision camera. * @param timestampSeconds The timestamp of the vision measurement in seconds. Note that if you don't use your * own time source by calling * {@link SwerveDrivePoseEstimator#updateWithTime(double, Rotation2d, * SwerveModulePosition[])}, then you must use a timestamp with an epoch since FPGA * startup (i.e., the epoch of this timestamp is the same epoch as * {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()}). This means that you should * use {@link edu.wpi.first.wpilibj.Timer#getFPGATimestamp()} as your time source in * this case. * @param visionMeasurementStdDevs Standard deviations of the vision pose measurement (x position in meters, y * position in meters, and heading in radians). Increase these numbers to trust the * vision pose measurement less. */ public void addVisionMeasurement( Pose2d visionRobotPoseMeters, double timestampSeconds, Matrix visionMeasurementStdDevs) { setVisionMeasurementStdDevs(visionMeasurementStdDevs); addVisionMeasurement(visionRobotPoseMeters, timestampSeconds); } /** * Updates the pose estimator with wheel encoder and gyro information. This should be called every loop. * * @param gyroAngle The current gyro angle. * @param gyroPitch The current gyro pitch. * @param gyroRoll The current gyro roll. * @param modulePositions The current distance measurements and rotations of the swerve modules. * @return The estimated pose of the robot in meters. */ public Pose2d update( Rotation2d gyroAngle, Rotation2d gyroPitch, Rotation2d gyroRoll, SwerveModulePosition[] modulePositions) { return updateWithTime( WPIUtilJNI.now() * 1.0e-6, gyroAngle, gyroPitch, gyroRoll, modulePositions); } /** * Updates the pose estimator with wheel encoder and gyro information. This should be called every loop. * * @param currentTimeSeconds Time at which this method was called, in seconds. * @param gyroAngle The current gyro angle. * @param gyroPitch The current gyro pitch. * @param gyroRoll The current gyro roll. * @param modulePositions The current distance measurements and rotations of the swerve modules. * @return The estimated pose of the robot in meters. */ public Pose2d updateWithTime( double currentTimeSeconds, Rotation2d gyroAngle, Rotation2d gyroPitch, Rotation2d gyroRoll, SwerveModulePosition[] modulePositions) { // super.updateWithTime(currentTimeSeconds, gyroAngle, modulePositions); if (modulePositions.length != m_numModules) { throw new IllegalArgumentException( "Number of modules is not consistent with number of wheel locations provided in " + "constructor"); } var internalModulePositions = new SwerveModulePosition[m_numModules]; for (int i = 0; i < m_numModules; i++) { internalModulePositions[i] = new SwerveModulePosition(modulePositions[i].distanceMeters, modulePositions[i].angle); } m_odometry.update(gyroAngle, gyroPitch, gyroRoll, internalModulePositions); m_poseBuffer.addSample( currentTimeSeconds, new InterpolationRecord( getEstimatedPosition(), gyroAngle, gyroPitch, gyroRoll, internalModulePositions)); return getEstimatedPosition(); } /** * Represents an odometry record. The record contains the inputs provided as well as the pose that was observed based * on these inputs, as well as the previous record and its inputs. */ private class InterpolationRecord implements Interpolatable { // The pose observed given the current sensor inputs and the previous pose. private final Pose2d poseMeters; // The current gyro angle. private final Rotation2d gyroAngle; // The current gyro pitch. private final Rotation2d gyroPitch; // The current gyro roll. private final Rotation2d gyroRoll; // The distances and rotations measured at each module. private final SwerveModulePosition[] modulePositions; /** * Constructs an Interpolation Record with the specified parameters. * * @param poseMeters The pose observed given the current sensor inputs and the previous pose. * @param gyro The current gyro angle. * @param gyroPitch The current gyro pitch. * @param gyroRoll The current gyro roll. * @param modulePositions The distances and rotations measured at each wheel. */ private InterpolationRecord( Pose2d poseMeters, Rotation2d gyro, Rotation2d gyroPitch, Rotation2d gyroRoll, SwerveModulePosition[] modulePositions) { this.poseMeters = poseMeters; this.gyroAngle = gyro; this.gyroPitch = gyroPitch; this.gyroRoll = gyroRoll; this.modulePositions = modulePositions; } /** * Return the interpolated record. This object is assumed to be the starting position, or lower bound. * * @param endValue The upper bound, or end. * @param t How far between the lower and upper bound we are. This should be bounded in [0, 1]. * @return The interpolated value. */ @Override public InterpolationRecord interpolate(InterpolationRecord endValue, double t) { if (t < 0) { return this; } else if (t >= 1) { return endValue; } else { // Find the new wheel distances. var modulePositions = new SwerveModulePosition[m_numModules]; // Find the distance travelled between this measurement and the interpolated measurement. var moduleDeltas = new SwerveModulePosition[m_numModules]; for (int i = 0; i < m_numModules; i++) { double ds = MathUtil.interpolate( this.modulePositions[i].distanceMeters, endValue.modulePositions[i].distanceMeters, t); Rotation2d theta = this.modulePositions[i].angle.interpolate(endValue.modulePositions[i].angle, t); modulePositions[i] = new SwerveModulePosition(ds, theta); moduleDeltas[i] = new SwerveModulePosition(ds - this.modulePositions[i].distanceMeters, theta); } // Find the new gyro angle. var gyro_lerp = gyroAngle.interpolate(endValue.gyroAngle, t); var gyroPitch_lerp = gyroPitch.interpolate(endValue.gyroPitch, t); var gyroRoll_lerp = gyroRoll.interpolate(endValue.gyroRoll, t); // Create a twist to represent this change based on the interpolated sensor inputs. Twist2d twist = m_kinematics.toTwist2d(moduleDeltas); twist.dtheta = gyro_lerp.minus(gyroAngle).getRadians(); return new InterpolationRecord( poseMeters.exp(twist), gyro_lerp, gyroPitch_lerp, gyroRoll_lerp, modulePositions); } } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof InterpolationRecord)) { return false; } InterpolationRecord record = (InterpolationRecord) obj; return Objects.equals(gyroAngle, record.gyroAngle) && Arrays.equals(modulePositions, record.modulePositions) && Objects.equals(poseMeters, record.poseMeters); } @Override public int hashCode() { return Objects.hash(gyroAngle, Arrays.hashCode(modulePositions), poseMeters); } } }