package frc.robot.subsystems; import static edu.wpi.first.units.Units.*; 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 com.pathplanner.lib.auto.AutoBuilder; import com.pathplanner.lib.config.PIDConstants; import com.pathplanner.lib.config.RobotConfig; import com.pathplanner.lib.controllers.PPHolonomicDriveController; import edu.wpi.first.math.Matrix; 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.TunerConstants.TunerConstants.TunerSwerveDrivetrain; /** * Class that extends the Phoenix 6 SwerveDrivetrain class and implements * Subsystem so it can easily be used in command-based projects. */ public class CommandSwerveDrivetrain extends TunerSwerveDrivetrain implements Subsystem { private static final double kSimLoopPeriod = 0.005; // 5 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; private final SwerveRequest.ApplyRobotSpeeds m_pathApplyRobotSpeeds = new SwerveRequest.ApplyRobotSpeeds(); /* 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 ) ); private void configureAutoBuilder() { try { var config = RobotConfig.fromGUISettings(); AutoBuilder.configure( () -> getState().Pose, // Supplier of current robot pose this::resetPose, // Consumer for seeding pose against auto () -> getState().Speeds, // Supplier of current robot speeds // Consumer of ChassisSpeeds and feedforwards to drive the robot (speeds, feedforwards) -> setControl( m_pathApplyRobotSpeeds.withSpeeds(speeds) .withWheelForceFeedforwardsX(feedforwards.robotRelativeForcesXNewtons()) .withWheelForceFeedforwardsY(feedforwards.robotRelativeForcesYNewtons()) ), new PPHolonomicDriveController( // PID constants for translation new PIDConstants(10, 0, 0), // PID constants for rotation new PIDConstants(7, 0, 0) ), config, // Assume the path needs to be flipped for Red vs Blue, this is normally the case () -> DriverStation.getAlliance().orElse(Alliance.Blue) == Alliance.Red, this // Subsystem for requirements ); } catch (Exception ex) { DriverStation.reportError("Failed to load PathPlanner config and configure AutoBuilder", ex.getStackTrace()); } } /* 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(); } configureAutoBuilder(); } /** * 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(); } configureAutoBuilder(); } /** * 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(); } configureAutoBuilder(); } /** * 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 requestSupplier) { return run(() -> this.setControl(requestSupplier.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); } }