package frc.robot.subsystems; import static edu.wpi.first.units.Units.*; import java.util.Optional; import java.util.function.Supplier; import com.ctre.phoenix6.SignalLogger; import com.ctre.phoenix6.Utils; import com.ctre.phoenix6.swerve.SwerveDrivetrainConstants; import com.ctre.phoenix6.swerve.SwerveModuleConstants; import com.ctre.phoenix6.swerve.SwerveRequest; import edu.wpi.first.math.Matrix; import edu.wpi.first.math.geometry.Pose2d; 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.DriverStation; import edu.wpi.first.wpilibj.DriverStation.Alliance; import edu.wpi.first.wpilibj.Notifier; import edu.wpi.first.wpilibj.RobotController; import edu.wpi.first.wpilibj2.command.Command; import edu.wpi.first.wpilibj2.command.Subsystem; import edu.wpi.first.wpilibj2.command.sysid.SysIdRoutine; import frc.robot.generated.TunerConstants.TunerSwerveDrivetrain; /** * Class that extends the Phoenix 6 SwerveDrivetrain class and implements * Subsystem so it can easily be used in command-based projects. * * Generated by the 2026 Tuner X Swerve Project Generator * https://v6.docs.ctr-electronics.com/en/stable/docs/tuner/tuner-swerve/index.html */ public class CommandSwerveDrivetrain extends TunerSwerveDrivetrain implements Subsystem { private static final double kSimLoopPeriod = 0.004; // 4 ms private Notifier m_simNotifier = null; private double m_lastSimTime; /* Blue alliance sees forward as 0 degrees (toward red alliance wall) */ private static final Rotation2d kBlueAlliancePerspectiveRotation = Rotation2d.kZero; /* Red alliance sees forward as 180 degrees (toward blue alliance wall) */ private static final Rotation2d kRedAlliancePerspectiveRotation = Rotation2d.k180deg; /* Keep track if we've ever applied the operator perspective before or not */ private boolean m_hasAppliedOperatorPerspective = false; /* Swerve requests to apply during SysId characterization */ private final SwerveRequest.SysIdSwerveTranslation m_translationCharacterization = new SwerveRequest.SysIdSwerveTranslation(); private final SwerveRequest.SysIdSwerveSteerGains m_steerCharacterization = new SwerveRequest.SysIdSwerveSteerGains(); private final SwerveRequest.SysIdSwerveRotation m_rotationCharacterization = new SwerveRequest.SysIdSwerveRotation(); /* SysId routine for characterizing translation. This is used to find PID gains for the drive motors. */ private final SysIdRoutine m_sysIdRoutineTranslation = new SysIdRoutine( new SysIdRoutine.Config( null, // Use default ramp rate (1 V/s) Volts.of(4), // Reduce dynamic step voltage to 4 V to prevent brownout null, // Use default timeout (10 s) // Log state with SignalLogger class state -> SignalLogger.writeString("SysIdTranslation_State", state.toString()) ), new SysIdRoutine.Mechanism( output -> setControl(m_translationCharacterization.withVolts(output)), null, this ) ); /* SysId routine for characterizing steer. This is used to find PID gains for the steer motors. */ private final SysIdRoutine m_sysIdRoutineSteer = new SysIdRoutine( new SysIdRoutine.Config( null, // Use default ramp rate (1 V/s) Volts.of(7), // Use dynamic voltage of 7 V null, // Use default timeout (10 s) // Log state with SignalLogger class state -> SignalLogger.writeString("SysIdSteer_State", state.toString()) ), new SysIdRoutine.Mechanism( volts -> setControl(m_steerCharacterization.withVolts(volts)), null, this ) ); /* * SysId routine for characterizing rotation. * This is used to find PID gains for the FieldCentricFacingAngle HeadingController. * See the documentation of SwerveRequest.SysIdSwerveRotation for info on importing the log to SysId. */ private final SysIdRoutine m_sysIdRoutineRotation = new SysIdRoutine( new SysIdRoutine.Config( /* This is in radians per second², but SysId only supports "volts per second" */ Volts.of(Math.PI / 6).per(Second), /* This is in radians per second, but SysId only supports "volts" */ Volts.of(Math.PI), null, // Use default timeout (10 s) // Log state with SignalLogger class state -> SignalLogger.writeString("SysIdRotation_State", state.toString()) ), new SysIdRoutine.Mechanism( output -> { /* output is actually radians per second, but SysId only supports "volts" */ setControl(m_rotationCharacterization.withRotationalRate(output.in(Volts))); /* also log the requested output for SysId */ SignalLogger.writeDouble("Rotational_Rate", output.in(Volts)); }, null, this ) ); /* The SysId routine to test */ private SysIdRoutine m_sysIdRoutineToApply = m_sysIdRoutineTranslation; /** * Constructs a CTRE SwerveDrivetrain using the specified constants. *

* This constructs the underlying hardware devices, so users should not construct * the devices themselves. If they need the devices, they can access them through * getters in the classes. * * @param drivetrainConstants Drivetrain-wide constants for the swerve drive * @param modules Constants for each specific module */ public CommandSwerveDrivetrain( SwerveDrivetrainConstants drivetrainConstants, SwerveModuleConstants... modules ) { super(drivetrainConstants, modules); if (Utils.isSimulation()) { startSimThread(); } } /** * Constructs a CTRE SwerveDrivetrain using the specified constants. *

* This constructs the underlying hardware devices, so users should not construct * the devices themselves. If they need the devices, they can access them through * getters in the classes. * * @param drivetrainConstants Drivetrain-wide constants for the swerve drive * @param odometryUpdateFrequency The frequency to run the odometry loop. If * unspecified or set to 0 Hz, this is 250 Hz on * CAN FD, and 100 Hz on CAN 2.0. * @param modules Constants for each specific module */ public CommandSwerveDrivetrain( SwerveDrivetrainConstants drivetrainConstants, double odometryUpdateFrequency, SwerveModuleConstants... modules ) { super(drivetrainConstants, odometryUpdateFrequency, modules); if (Utils.isSimulation()) { startSimThread(); } } /** * Constructs a CTRE SwerveDrivetrain using the specified constants. *

* This constructs the underlying hardware devices, so users should not construct * the devices themselves. If they need the devices, they can access them through * getters in the classes. * * @param drivetrainConstants Drivetrain-wide constants for the swerve drive * @param odometryUpdateFrequency The frequency to run the odometry loop. If * unspecified or set to 0 Hz, this is 250 Hz on * CAN FD, and 100 Hz on CAN 2.0. * @param odometryStandardDeviation The standard deviation for odometry calculation * in the form [x, y, theta]ᵀ, with units in meters * and radians * @param visionStandardDeviation The standard deviation for vision calculation * in the form [x, y, theta]ᵀ, with units in meters * and radians * @param modules Constants for each specific module */ public CommandSwerveDrivetrain( SwerveDrivetrainConstants drivetrainConstants, double odometryUpdateFrequency, Matrix odometryStandardDeviation, Matrix visionStandardDeviation, SwerveModuleConstants... modules ) { super(drivetrainConstants, odometryUpdateFrequency, odometryStandardDeviation, visionStandardDeviation, modules); if (Utils.isSimulation()) { startSimThread(); } } /** * Returns a command that applies the specified control request to this swerve drivetrain. * * @param request Function returning the request to apply * @return Command to run */ public Command applyRequest(Supplier request) { return run(() -> this.setControl(request.get())); } /** * Runs the SysId Quasistatic test in the given direction for the routine * specified by {@link #m_sysIdRoutineToApply}. * * @param direction Direction of the SysId Quasistatic test * @return Command to run */ public Command sysIdQuasistatic(SysIdRoutine.Direction direction) { return m_sysIdRoutineToApply.quasistatic(direction); } /** * Runs the SysId Dynamic test in the given direction for the routine * specified by {@link #m_sysIdRoutineToApply}. * * @param direction Direction of the SysId Dynamic test * @return Command to run */ public Command sysIdDynamic(SysIdRoutine.Direction direction) { return m_sysIdRoutineToApply.dynamic(direction); } @Override public void periodic() { /* * Periodically try to apply the operator perspective. * If we haven't applied the operator perspective before, then we should apply it regardless of DS state. * This allows us to correct the perspective in case the robot code restarts mid-match. * Otherwise, only check and apply the operator perspective if the DS is disabled. * This ensures driving behavior doesn't change until an explicit disable event occurs during testing. */ if (!m_hasAppliedOperatorPerspective || DriverStation.isDisabled()) { DriverStation.getAlliance().ifPresent(allianceColor -> { setOperatorPerspectiveForward( allianceColor == Alliance.Red ? kRedAlliancePerspectiveRotation : kBlueAlliancePerspectiveRotation ); m_hasAppliedOperatorPerspective = true; }); } } private void startSimThread() { m_lastSimTime = Utils.getCurrentTimeSeconds(); /* Run simulation at a faster rate so PID gains behave more reasonably */ m_simNotifier = new Notifier(() -> { final double currentTime = Utils.getCurrentTimeSeconds(); double deltaTime = currentTime - m_lastSimTime; m_lastSimTime = currentTime; /* use the measured time delta, get battery voltage from WPILib */ updateSimState(deltaTime, RobotController.getBatteryVoltage()); }); m_simNotifier.startPeriodic(kSimLoopPeriod); } /** * Adds a vision measurement to the Kalman Filter. This will correct the odometry pose estimate * while still accounting for measurement noise. * * @param visionRobotPoseMeters The pose of the robot as measured by the vision camera. * @param timestampSeconds The timestamp of the vision measurement in seconds. */ @Override public void addVisionMeasurement(Pose2d visionRobotPoseMeters, double timestampSeconds) { super.addVisionMeasurement(visionRobotPoseMeters, Utils.fpgaToCurrentTime(timestampSeconds)); } /** * Adds a vision measurement to the Kalman Filter. This will correct the odometry pose estimate * while still accounting for measurement noise. *

* Note that the vision measurement standard deviations passed into this method * will continue to apply to future measurements until a subsequent call to * {@link #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. * @param visionMeasurementStdDevs Standard deviations of the vision pose measurement * in the form [x, y, theta]ᵀ, with units in meters and radians. */ @Override public void addVisionMeasurement( Pose2d visionRobotPoseMeters, double timestampSeconds, Matrix visionMeasurementStdDevs ) { super.addVisionMeasurement(visionRobotPoseMeters, Utils.fpgaToCurrentTime(timestampSeconds), visionMeasurementStdDevs); } /** * Return the pose at a given timestamp, if the buffer is not empty. * * @param timestampSeconds The timestamp of the pose in seconds. * @return The pose at the given timestamp (or Optional.empty() if the buffer is empty). */ @Override public Optional samplePoseAt(double timestampSeconds) { return super.samplePoseAt(Utils.fpgaToCurrentTime(timestampSeconds)); } }