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.swerve;
006
007import static org.wpilib.units.Units.*;
008
009import com.stuypulse.robot.constants.GlobalSettings;
010import com.stuypulse.robot.constants.GlobalSettings.RobotMode;
011import com.stuypulse.robot.subsystems.vision.Vision.VisionConsumer;
012import com.stuypulse.robot.util.PeriodicSubsystem;
013
014import org.wpilib.driverstation.RobotState;
015import org.wpilib.math.estimator.SwerveDrivePoseEstimator;
016import org.wpilib.math.geometry.Pose2d;
017import org.wpilib.math.geometry.Rotation2d;
018import org.wpilib.math.geometry.Translation2d;
019import org.wpilib.math.geometry.Twist2d;
020import org.wpilib.math.kinematics.ChassisVelocities;
021import org.wpilib.math.kinematics.SwerveDriveKinematics;
022import org.wpilib.math.kinematics.SwerveModulePosition;
023import org.wpilib.math.kinematics.SwerveModuleVelocity;
024import org.wpilib.util.Alert;
025import org.wpilib.util.UsageReporting;
026
027import java.util.Optional;
028import java.util.concurrent.locks.Lock;
029import java.util.concurrent.locks.ReentrantLock;
030import org.littletonrobotics.junction.AutoLogOutput;
031import org.littletonrobotics.junction.Logger;
032
033// import org.wpilib.wpilibj2.command.sysid.SysIdRoutine;
034
035public class Swerve extends PeriodicSubsystem {
036    // TunerConstants doesn't include these constants, so they are declared locally
037    static final double ODOMETRY_FREQUENCY = TunerConstants.kCANBus.isNetworkFD() ? 250.0 : 100.0;
038    public static final double DRIVE_BASE_RADIUS =
039            Math.max(
040                    Math.max(
041                            Math.hypot(
042                                    TunerConstants.FrontLeft.LocationX,
043                                    TunerConstants.FrontLeft.LocationY),
044                            Math.hypot(
045                                    TunerConstants.FrontRight.LocationX,
046                                    TunerConstants.FrontRight.LocationY)),
047                    Math.max(
048                            Math.hypot(
049                                    TunerConstants.BackLeft.LocationX,
050                                    TunerConstants.BackLeft.LocationY),
051                            Math.hypot(
052                                    TunerConstants.BackRight.LocationX,
053                                    TunerConstants.BackRight.LocationY)));
054
055    static final Lock odometryLock = new ReentrantLock();
056    private final GyroIO gyroIO;
057    private final GyroIOInputsAutoLogged gyroInputs = new GyroIOInputsAutoLogged();
058    private final Module[] modules = new Module[4]; // FL, FR, BL, BR
059    // private final SysIdRoutine sysId;
060    private final Alert gyroDisconnectedAlert =
061            new Alert(
062                    "SwerveGyroDisconnect",
063                    "Disconnected gyro, using kinematics as fallback.",
064                    Alert.Level.HIGH);
065
066    private SwerveDriveKinematics kinematics = new SwerveDriveKinematics(getModuleTranslations());
067    private Rotation2d rawGyroRotation = Rotation2d.ZERO;
068    private SwerveModulePosition[] lastModulePositions = // For delta tracking
069            new SwerveModulePosition[] {
070                new SwerveModulePosition(),
071                new SwerveModulePosition(),
072                new SwerveModulePosition(),
073                new SwerveModulePosition()
074            };
075    private SwerveDrivePoseEstimator poseEstimator =
076            new SwerveDrivePoseEstimator(
077                    kinematics, rawGyroRotation, lastModulePositions, Pose2d.ZERO);
078
079    public Swerve(
080            GyroIO gyroIO,
081            ModuleIO flModuleIO,
082            ModuleIO frModuleIO,
083            ModuleIO blModuleIO,
084            ModuleIO brModuleIO) {
085        this.gyroIO = gyroIO;
086        modules[0] = new Module(flModuleIO, 0, TunerConstants.FrontLeft);
087        modules[1] = new Module(frModuleIO, 1, TunerConstants.FrontRight);
088        modules[2] = new Module(blModuleIO, 2, TunerConstants.BackLeft);
089        modules[3] = new Module(brModuleIO, 3, TunerConstants.BackRight);
090
091        // Usage reporting for swerve template
092        UsageReporting.reportUsage("RobotDrive", "RobotDriveSwerve_AdvantageKit");
093
094        // Start odometry thread
095        PhoenixOdometryThread.getInstance().start();
096
097        // Configure AutoBuilder for PathPlanner
098        // AutoBuilder.configure(
099        // this::getPose,
100        // this::setPose,
101        // this::getChassisSpeeds,
102        // this::runVelocity,
103        // new PPHolonomicDriveController(
104        // new PIDConstants(5.0, 0.0, 0.0), new PIDConstants(5.0, 0.0, 0.0)),
105        // PP_CONFIG,
106        // () -> DriverStation.getAlliance().orElse(Alliance.Blue) == Alliance.Red,
107        // this);
108        // Pathfinding.setPathfinder(new LocalADStarAK());
109        // PathPlannerLogging.setLogActivePathCallback(
110        // (activePath) -> {
111        // Logger.recordOutput("Odometry/Trajectory", activePath.toArray(new
112        // Pose2d[0]));
113        // });
114        // PathPlannerLogging.setLogTargetPoseCallback(
115        // (targetPose) -> {
116        // Logger.recordOutput("Odometry/TrajectorySetpoint", targetPose);
117        // });
118
119        // Configure SysId
120        // sysId =
121        // new SysIdRoutine(
122        // new SysIdRoutine.Config(
123        // null,
124        // null,
125        // null,
126        // (state) -> Logger.recordOutput("Drive/SysIdState", state.toString())),
127        // new SysIdRoutine.Mechanism(
128        // (voltage) -> runCharacterization(voltage.in(Volts)), null, this));
129    }
130
131    @Override
132    public void periodic() {
133        odometryLock.lock(); // Prevents odometry updates while reading data
134        gyroIO.updateInputs(gyroInputs);
135        Logger.processInputs("Drive/Gyro", gyroInputs);
136        for (var module : modules) {
137            module.periodic();
138        }
139        odometryLock.unlock();
140
141        // Stop moving when disabled
142        if (RobotState.isDisabled()) {
143            for (var module : modules) {
144                module.stop();
145            }
146        }
147
148        // Log empty setpoint states when disabled
149        if (RobotState.isDisabled()) {
150            Logger.recordOutput("SwerveStates/Setpoints", new SwerveModuleVelocity[] {});
151            Logger.recordOutput("SwerveStates/SetpointsOptimized", new SwerveModuleVelocity[] {});
152        }
153
154        // Update odometry
155        double[] sampleTimestamps =
156                modules[0].getOdometryTimestamps(); // All signals are sampled together
157        int sampleCount = sampleTimestamps.length;
158        for (int i = 0; i < sampleCount; i++) {
159            // Read wheel positions and deltas from each module
160            SwerveModulePosition[] modulePositions = new SwerveModulePosition[4];
161            SwerveModulePosition[] moduleDeltas = new SwerveModulePosition[4];
162            for (int moduleIndex = 0; moduleIndex < 4; moduleIndex++) {
163                modulePositions[moduleIndex] = modules[moduleIndex].getOdometryPositions()[i];
164                moduleDeltas[moduleIndex] =
165                        new SwerveModulePosition(
166                                modulePositions[moduleIndex].distance
167                                        - lastModulePositions[moduleIndex].distance,
168                                modulePositions[moduleIndex].angle);
169                lastModulePositions[moduleIndex] = modulePositions[moduleIndex];
170            }
171
172            // Update gyro angle
173            if (gyroInputs.connected) {
174                // Use the real gyro angle
175                rawGyroRotation = gyroInputs.odometryYawPositions[i];
176            } else {
177                // Use the angle delta from the kinematics and module deltas
178                Twist2d twist = kinematics.toTwist2d(moduleDeltas);
179                rawGyroRotation = rawGyroRotation.plus(new Rotation2d(twist.dtheta));
180            }
181
182            // Apply update
183            poseEstimator.updateWithTime(sampleTimestamps[i], rawGyroRotation, modulePositions);
184        }
185
186        // Update gyro alert
187        gyroDisconnectedAlert.set(
188                !gyroInputs.connected && GlobalSettings.CURRENT_MODE != RobotMode.SIM);
189    }
190
191    @Override
192    public void periodicAfterScheduler() {
193        for (Module module : modules) {
194            module.periodicAfterScheduler();
195        }
196    }
197
198    /**
199     * Runs the drive at the desired velocity.
200     *
201     * @param speeds Speeds in meters/sec
202     */
203    public void runVelocity(ChassisVelocities speeds) {
204        // Calculate module setpoints
205        ChassisVelocities discreteSpeeds = speeds.discretize(0.02);
206        SwerveModuleVelocity[] setpointStates = kinematics.toSwerveModuleVelocities(discreteSpeeds);
207        setpointStates =
208                SwerveDriveKinematics.desaturateWheelVelocities(
209                        setpointStates, TunerConstants.kSpeedAt12Volts);
210
211        // Log unoptimized setpoints and setpoint speeds
212        Logger.recordOutput("SwerveStates/Setpoints", setpointStates);
213        Logger.recordOutput("SwerveChassisSpeeds/Setpoints", discreteSpeeds);
214
215        // Send setpoints to modules
216        for (int i = 0; i < 4; i++) {
217            modules[i].runSetpoint(setpointStates[i]);
218        }
219
220        // Log optimized setpoints (runSetpoint mutates each state)
221        Logger.recordOutput("SwerveStates/SetpointsOptimized", setpointStates);
222    }
223
224    /** Runs the drive in a straight line with the specified drive output. */
225    public void runCharacterization(double output) {
226        for (int i = 0; i < 4; i++) {
227            modules[i].runCharacterization(output);
228        }
229    }
230
231    /** Stops the drive. */
232    public void stop() {
233        runVelocity(new ChassisVelocities());
234    }
235
236    /**
237     * Stops the drive and turns the modules to an X arrangement to resist movement. The modules
238     * will return to their normal orientations the next time a nonzero velocity is requested.
239     */
240    public void stopWithX() {
241        Rotation2d[] headings = new Rotation2d[4];
242        for (int i = 0; i < 4; i++) {
243            Optional<Rotation2d> rotation = getModuleTranslations()[i].getAngle();
244            headings[i] = rotation.isPresent() ? rotation.get() : headings[i];
245        }
246        kinematics.resetHeadings(headings);
247        stop();
248    }
249
250    /** Returns a command to run a quasistatic test in the specified direction. */
251    // public Command sysIdQuasistatic(SysIdRoutine.Direction direction) {
252    //     return run(() -> runCharacterization(0.0))
253    //             .withTimeout(1.0)
254    //             .andThen(sysId.quasistatic(direction));
255    // }
256
257    // /** Returns a command to run a dynamic test in the specified direction. */
258    // public Command sysIdDynamic(SysIdRoutine.Direction direction) {
259    //     return run(() ->
260    // runCharacterization(0.0)).withTimeout(1.0).andThen(sysId.dynamic(direction));
261    // }
262
263    /** Returns the module states (turn angles and drive velocities) for all of the modules. */
264    @AutoLogOutput(key = "SwerveStates/Measured")
265    private SwerveModuleVelocity[] getModuleVelocities() {
266        SwerveModuleVelocity[] states = new SwerveModuleVelocity[4];
267        for (int i = 0; i < 4; i++) {
268            states[i] = modules[i].getVelocity();
269        }
270        return states;
271    }
272
273    /** Returns the module positions (turn angles and drive positions) for all of the modules. */
274    private SwerveModulePosition[] getModulePositions() {
275        SwerveModulePosition[] states = new SwerveModulePosition[4];
276        for (int i = 0; i < 4; i++) {
277            states[i] = modules[i].getPosition();
278        }
279        return states;
280    }
281
282    /** Returns the measured chassis speeds of the robot. */
283    @AutoLogOutput(key = "SwerveChassisSpeeds/Measured")
284    private ChassisVelocities getChassisSpeeds() {
285        return kinematics.toChassisVelocities(getModuleVelocities());
286    }
287
288    /** Returns the position of each module in radians. */
289    public double[] getWheelRadiusCharacterizationPositions() {
290        double[] values = new double[4];
291        for (int i = 0; i < 4; i++) {
292            values[i] = modules[i].getWheelRadiusCharacterizationPosition();
293        }
294        return values;
295    }
296
297    /** Returns the average velocity of the modules in rotations/sec (Phoenix native units). */
298    public double getFFCharacterizationVelocity() {
299        double output = 0.0;
300        for (int i = 0; i < 4; i++) {
301            output += modules[i].getFFCharacterizationVelocity() / 4.0;
302        }
303        return output;
304    }
305
306    /** Returns the current odometry pose. */
307    @AutoLogOutput(key = "Odometry/Robot")
308    public Pose2d getPose() {
309        return poseEstimator.getEstimatedPosition();
310    }
311
312    /** Returns the current odometry rotation. */
313    public Rotation2d getRotation() {
314        return getPose().getRotation();
315    }
316
317    /** Resets the current odometry pose. */
318    public void resetPose(Pose2d pose) {
319        poseEstimator.resetPosition(rawGyroRotation, getModulePositions(), pose);
320    }
321
322    public void resetRotation(Rotation2d rotation) {
323        poseEstimator.resetRotation(rotation);
324    }
325
326    public VisionConsumer visionConsumer = poseEstimator::addVisionMeasurement;
327
328    /** Returns the maximum linear speed in meters per sec. */
329    public double getMaxLinearSpeedMetersPerSec() {
330        return TunerConstants.kSpeedAt12Volts.in(MetersPerSecond);
331    }
332
333    /** Returns the maximum angular speed in radians per sec. */
334    public double getMaxAngularSpeedRadPerSec() {
335        return getMaxLinearSpeedMetersPerSec() / DRIVE_BASE_RADIUS;
336    }
337
338    /** Returns an array of module translations. */
339    public static Translation2d[] getModuleTranslations() {
340        return new Translation2d[] {
341            new Translation2d(
342                    TunerConstants.FrontLeft.LocationX, TunerConstants.FrontLeft.LocationY),
343            new Translation2d(
344                    TunerConstants.FrontRight.LocationX, TunerConstants.FrontRight.LocationY),
345            new Translation2d(TunerConstants.BackLeft.LocationX, TunerConstants.BackLeft.LocationY),
346            new Translation2d(
347                    TunerConstants.BackRight.LocationX, TunerConstants.BackRight.LocationY)
348        };
349    }
350}