Swerve Module Example

Here is an example implementation of a swerve module using Nova. The turn controller is configured with an external absolute encoder. You would create a swerve module by calling the constructor as follows.

// Assume module1 is defined in class scope
module1 = new NovaSwerveModule(1, 4, 77); // drive ID, turn ID, encoder offset.

Below is the swerve class definition.

public class NovaSwerveModule {
  // Constants for drive and turn motors (use your own)
  private static final double MAX_VELOCITY = 4.0; // max velocity
  private static final double GEAR_RATIO = 6.75; // gear ratio drive motor to wheel
  private static final double WHEEL_CIRCUM = .1; // wheel circumference
  
  // Thrify Novas driving NEO
  private ThriftyNova drive;
  private ThriftyNova turn;

  // Unit convertors for drive velocity, drive position, and turn
  private Conversion driveConverter; 
  private Conversion distanceConverter; 
  private Conversion turnConverter; 
  
  // Store the turn encoder offset
  private double turnEncoderOffset;
  
  // Construct swerve module
  private NovaSwerveModule(int driveId, int turnId, double turnEncoderOffset) {
    this.turnEncoderOffset = turnEncoderOffset;
    initializeDriveMotor(driveId);
    initializeTurnMotor(turnId);
  }

  // Construct and configure turn motor
  private void initializeDriveMotor(int driveId) {

    // For drive we need to convert between RPM and internal NEO encoder units
    driveConverter = new Conversion(VelcotiyUnit.ROTATIONS_PER_MIN, EncoderType.INTERNAL);
    distanceConverter = new Conversion(PositionUnit.ROTATIONS, EncoderType.INTERNAL);

    drive = new ThriftyNova(driveId) // create with correct CAN ID
      .setBrakeMode(true) // brake mode
      .setInverted(false) // not inverted 
      .setRampUp(0.1)    // small ramp up used during auto 
      .setRampDown(0.1)  // same for ramp down
      .setMaxOutput(1, 1)  // full power because drive motor
      .setMaxCurrent(CurrentType.SUPPLY, 80) // set a 80amp current limit
                                             // on supply side
      .useEncoderType(EncoderType.INTERNAL) // use internal NEO encoder
      .usePIDSlot(PIDSlot.SLOT0)            // use the first PID slot
      
      // Configure the first PID slot (examples, please tune your own values)
      .pid0.setP(0.5) 
      .pid0.setI(0)
      .pid0.setD(0)
      .pid0.setFF(1.2);
      
    // Iterate through errors and check them
    for (var err : drive.getErrors()) {
    // The user can handle the errors
      System.err.println("Drive Motor Config Error: " + err.toString());
    }
    // Clear errors here
    drive.clearErrors();
  }

  // Construct and configure turn motor
  private void initializeTurnMotor(int turnId) {

    // For drive we need to convert between radians and external absolute encoder units
    turnConverter = new Conversion(PositionUnit.RADIANS, EncoderType.ABS);

    turn = new ThriftyNova(turnId) // create with correct CAN ID
      .setBrakeMode(true) // brake mode
      .setInverted(false) // not inverted 
      .setMaxOutput(1, 1)  // full power because we sometimes want to
                           // change direction quickly
      .setMaxCurrent(CurrentType.SUPPLY, 35) // set a 35amp current limit
                                             // on supply side
      .useEncoderType(EncoderType.ABS)      // use external absolute encoder
      .usePIDSlot(PIDSlot.SLOT0)            // use the first PID slot
      
      // Configure the first PID slot (examples, please tune your own values)
      .pid0.setP(0.5) 
      .pid0.setI(0)
      .pid0.setD(0)
      .pid0.setFF(1.2);
      
    // Iterate through errors and check them
    for (var err : turn.getErrors()) {
    // The user can handle the errors
      System.err.println("Drive Motor Config Error: " + err.toString());
    }
    // Clear errors here
    turn.clearErrors();
  }
  
  
  // This method is called periodically
  public void getState() {
    // Set the position based on the setpoint
    pivot.setPosition(convertor.toMotor(setPoint.pos / inchesPerRad));
    
    // Get the real position from the encoder
    double realPosition = convertor.fromMotor(pivot.getPosition()) * inchesPerRad;
    
    // Log the real position and the current draw
    SmartDashboard.putNumber("Elevator Position", realPosition); 
    SmartDashboard.putNumber("Elevator Current", pivot.getCurrent()); 
  }

  // Get position, required for swerve
  public SwerveModulePosition getPosition() {
    // Get the position of the drive motor and use the distance convertor to get the real position
    double distance = distanceConverter.fromMotor((drive.getPosition() * WHEEL_CIRCUM) / GEAR_RATIO);
    // Return the position object using the distance and rotation
    return new SwerveModulePosition(distance, getRotation());
  }

  // Get rotation, required for swerve
  public Rotation2d getRotation() {
    // Return the current rotation of the swerve rotation
    return Rotation2d.fromRadians(turnConverter.fromMotor(turn.getPosition()) - turnEncoderOffset);
  }

  // Set desired state, required for swerve
  public void setDesiredState(SwerveModuleState state) {
    // Optimize the state
    final SwerveModuleState optimized = SwerveModuleState.optimize(state, getRotation());

    // Output the desired velocity on the drive motor
    if (DriverStation.isAutonomous()) {
      // Calculate the motor velocity input
      double driveVelocity = driveConvertor.toMotor((optimized.speedMetersPerSecond * GEAR_RATIO) / WHEEL_CIRCUM);
      drive.setVelocity(driveVelocity);
    } else {
      // Calculate the motor power input
      drive.setPercent(optimized.speedMetersPerSecond / MAX_VELOCITY);
    }

    // Calculate the turn position input
    double turnPosition = turnConvertor.toMotor(optimized.angle.getRadians());
    // Output the desired angle on the turn motor
    turn.setPosition(turnPosition);
  }
}

Last updated