Turn Command

Next we are going to create a command called TurnCommand which will allow us to turn the robot in place by a specified angle. You should know enough now to create the framework for this command. We will want to specify the speed to be used in the turn as well as the angle so create the constructor for your class so that it takes two double parameters, the first to specify the speed and the second to specify the angle. After you have created the new command, compare you code to the code below.

.

.

.

.

/*----------------------------------------------------------------------------*/
/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
/* Open Source Software - may be modified and shared by FRC teams. The code   */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project.                                                               */
/*----------------------------------------------------------------------------*/

package robot.commands;

import edu.wpi.first.wpilibj2.command.CommandBase;
import robot.subsystems.DriveSubsystem;
import robotCore.Logger;

/**
 * An example command that uses an example subsystem.
 */
public class TurnCommand extends CommandBase {
  private final DriveSubsystem m_subsystem;
  private double m_speed;
  private double m_angle;

  /**
   * Creates a new TurnCommand.
   *
   * @param subsystem The subsystem used by this command.
   */
  public TurnCommand(DriveSubsystem subsystem, double speed, double angle) {
    Logger.Log("TurnCommand", 3, "TurnCommand()");

    m_subsystem = subsystem;
    m_speed = speed;
    m_angle = angle;
    // Use addRequirements() here to declare subsystem dependencies.
    addRequirements(m_subsystem);
  }

  // Called when the command is initially scheduled.
  @Override
  public void initialize() {
    Logger.Log("TurnCommand", 2, "initialize()");
  }

  // Called every time the scheduler runs while the command is scheduled.
  @Override
  public void execute() {
    Logger.Log("TurnCommand", -1, "execute()");
  }

  // Called once the command ends or is interrupted.
  @Override
  public void end(boolean interrupted) {
    Logger.Log("TurnCommand", 2, String.format("end(%b)", interrupted));
  }

  // Returns true when the command should end.
  @Override
  public boolean isFinished() {
    Logger.Log("TurnCommand", -1, "isFinished()");
    return false;
  }
}

Did you create some member variables (e.g. m_speed and m_angle) to store the speed and angle that are passed in on the constructor?

Now we are going to use the encoders to measure the turn. Once again, we will want to use copies of the encoders like we did in our DriveForDistanceCommand. Add code to declare and initialize the left and right encoder:

.

.

.

.

public class TurnCommand extends CommandBase {
  private final DriveSubsystem m_subsystem;
  private double m_speed;
  private double m_angle;
  private Encoder m_leftEncoder;
  private Encoder m_rightEncoder;

  /**
   * Creates a new TurnCommand.
   *
   * @param subsystem The subsystem used by this command.
   */
  public TurnCommand(DriveSubsystem subsystem, double speed, double angle) {
    Logger.Log("TurnCommand", 3, "TurnCommand()");

    m_subsystem = subsystem;
    m_speed = speed;
    m_angle = angle;
    m_leftEncoder = m_subsystem.getLeftEncoder();
    m_rightEncoder = m_subsystem.getRightEncoder();

    // Use addRequirements() here to declare subsystem dependencies.
    addRequirements(m_subsystem);
  }

Now in the initialize() function we also want reset the encoders and turn the motors on. To make the robot turn, we need to set the speed to the left and right motors opposite, and we need to set the signs so that it turns in the correct direction. Let’s say that a positive angle should make the robot turn to the right (clockwise) and a negative angle should make the robot turn to the left (anticlockwise). See if you can add code to your initialize() function to accomplish this and then check your result with the one below.

.

.

.

.

  public void initialize() {
    Logger.Log("TurnCommand", 2, "initialize()");

    m_leftEncoder.reset();
    m_rightEncoder.reset();

    if (m_angle < 0)
    {
        m_subsystem.setSpeed(-m_speed, m_speed);    // Turn left
    }
    else
    {
        m_subsystem.setSpeed(m_speed, -m_speed);    // Turn right
    }
  }

Now we need to add some code to the isFinished() function to return true when the robot has turned by the requested angle. Add the following line to the isFinished() function:

Note that delta will be positive if the robot is turning right (i.e. the left wheel is moving forward and the right wheel is moving backward), and negative if the robot is turning left. The magnitude of delta will also be a measure of how far the robot is turned. So lets return true when the magnitude of delta exceeds the magnitude of the angle.

  public boolean isFinished() {
    Logger.Log("TurnCommand", -1, "isFinished()");

    int delta = m_leftEncoder.get() - m_rightEncoder.get();

    return (Math.abs(delta) >= Math.abs(m_angle));
  }

Note that we are using the absolute value of both delta and m_angle in the comparison since they can both be negative.

Remember to turn the motors back off in the end() function. Your TurnCommand.java file should now look like:

/*----------------------------------------------------------------------------*/
/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
/* Open Source Software - may be modified and shared by FRC teams. The code   */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project.                                                               */
/*----------------------------------------------------------------------------*/

package robot.commands;

import edu.wpi.first.wpilibj2.command.CommandBase;
import robot.subsystems.DriveSubsystem;
import robotCore.Logger;
import robotCore.Encoder;

/**
 * An example command that uses an example subsystem.
 */
public class TurnCommand extends CommandBase {
  private final DriveSubsystem m_subsystem;
  private double m_speed;
  private double m_angle;
  private Encoder m_leftEncoder;
  private Encoder m_rightEncoder;

  /**
   * Creates a new TurnCommand.
   *
   * @param subsystem The subsystem used by this command.
   */
  public TurnCommand(DriveSubsystem subsystem, double speed, double angle) {
    Logger.Log("TurnCommand", 3, "TurnCommand()");

    m_subsystem = subsystem;
    m_speed = speed;
    m_angle = angle;
    m_leftEncoder = m_subsystem.getLeftEncoder();
    m_rightEncoder = m_subsystem.getRightEncoder();

    // Use addRequirements() here to declare subsystem dependencies.
    addRequirements(m_subsystem);
  }

  // Called when the command is initially scheduled.
  @Override
  public void initialize() {
    Logger.Log("TurnCommand", 2, "initialize()");

    m_leftEncoder.reset();
    m_rightEncoder.reset();

    if (m_angle < 0) {
      m_subsystem.setSpeed(-m_speed, m_speed); // Turn left
    } else {
      m_subsystem.setSpeed(m_speed, -m_speed); // Turn right
    }
  }

  // Called every time the scheduler runs while the command is scheduled.
  @Override
  public void execute() {
    Logger.Log("TurnCommand", -1, "execute()");
  }

  // Called once the command ends or is interrupted.
  @Override
  public void end(boolean interrupted) {
    Logger.Log("TurnCommand", 2, String.format("end(%b)", interrupted));

    m_subsystem.setPower(0, 0);
  }

  // Returns true when the command should end.
  @Override
  public boolean isFinished() {
    Logger.Log("TurnCommand", -1, "isFinished()");

    int delta = m_leftEncoder.get() - m_rightEncoder.get();

    return (Math.abs(delta) >= Math.abs(m_angle));
  }
}

Note that if we want to be able to set the angle in degrees, we will need to figure out a conversion factor that will convert degrees to the arbitrary encoder units returned by the encoders. For now we will pass in an angle in the encoder units and see how far the robot turns. Configure the RobotContainer class so that this command will be executed with 0.5 speed for and angle of 2000 when we press button 5 on the joystick. When you have made that change, compare it to the code below.

.

.

.

.

/*----------------------------------------------------------------------------*/
/* Copyright (c) 2018-2019 FIRST. All Rights Reserved.                        */
/* Open Source Software - may be modified and shared by FRC teams. The code   */
/* must be accompanied by the FIRST BSD license file in the root directory of */
/* the project.                                                               */
/*----------------------------------------------------------------------------*/

package robot;

import edu.wpi.first.wpilibj2.command.Command;
import edu.wpi.first.wpilibj2.command.button.JoystickButton;
import robot.commands.ArcadeDriveCommand;
import robot.commands.DriveForDistanceCommand;
import robot.commands.DriveForTimeCommand;
import robot.commands.ExampleCommand;
import robot.commands.TestMotorSpeedCommand;
import robot.commands.TestSpeedControlCommand;
import robot.commands.TurnCommand;
import robot.subsystems.DriveSubsystem;
import robot.subsystems.ExampleSubsystem;
import robotCore.Joystick;

/**
 * This class is where the bulk of the robot should be declared. Since
 * Command-based is a "declarative" paradigm, very little robot logic should
 * actually be handled in the {@link Robot} periodic methods (other than the
 * scheduler calls). Instead, the structure of the robot (including subsystems,
 * commands, and button mappings) should be declared here.
 */
public class RobotContainer {
  // The robot's subsystems and commands are defined here...
  @SuppressWarnings("unused")
  private final ExampleSubsystem m_exampleSubsystem = new ExampleSubsystem();
  private final DriveSubsystem m_driveSubsystem = new DriveSubsystem();
  private final Joystick m_joystick = new Joystick(0);
  private final JoystickButton m_button1 = new JoystickButton(m_joystick, 1);
  private final JoystickButton m_button2 = new JoystickButton(m_joystick, 2);
  private final JoystickButton m_button3 = new JoystickButton(m_joystick, 3);
  private final JoystickButton m_button4 = new JoystickButton(m_joystick, 4);
  private final JoystickButton m_button5 = new JoystickButton(m_joystick, 5);

  private final ExampleCommand m_autoCommand = null; // new ExampleCommand(m_exampleSubsystem);

  /**
   * The container for the robot. Contains subsystems, OI devices, and commands.
   */
  public RobotContainer() {
    m_driveSubsystem.setDefaultCommand(new ArcadeDriveCommand(m_driveSubsystem, m_joystick));

    // Configure the button bindings
    configureButtonBindings();
  }

  /**
   * Use this method to define your button->command mappings. Buttons can be
   * created by instantiating a {@link GenericHID} or one of its subclasses
   * ({@link edu.wpi.first.wpilibj.Joystick} or {@link XboxController}), and then
   * passing it to a {@link edu.wpi.first.wpilibj2.command.button.JoystickButton}.
   */
  private void configureButtonBindings() {
    m_button1.whenPressed(new DriveForTimeCommand(m_driveSubsystem, 0.75, 3.0));
    m_button2.whenPressed(new DriveForDistanceCommand(m_driveSubsystem, 0.75, 20));
    m_button3.whenPressed(new TestMotorSpeedCommand(m_driveSubsystem));
    m_button4.whileHeld(new TestSpeedControlCommand(m_driveSubsystem));
    m_button5.whenPressed(new TurnCommand(m_driveSubsystem, 0.5, 2000));
  }

  /**
   * Use this to pass the autonomous command to the main {@link Robot} class.
   *
   * @return the command to run in autonomous
   */
  public Command getAutonomousCommand() {
    // An ExampleCommand will run in autonomous
    return m_autoCommand;
  }
}

Now deploy and run your program and see how far the robot turns. Try and find a number to replace the 2000 value that will make the robot turn exactly 360 degrees.

.

.

.

.

You should find that approximately 4900 encoder units will make the robot turn 360 degrees. This means that if the input angle is in degrees, we need to multiply it by (4900 / 360) to get the number of encoder units that we need. Implementing this in the code we get:

  public TurnCommand(DriveSubsystem subsystem, double speed, double angle) {
    Logger.Log("TurnCommand", 3, "TurnCommand()");

    m_subsystem = subsystem;
    m_speed = speed;
    m_angle = (angle * 4900) / 360;
    m_leftEncoder = m_subsystem.getLeftEncoder();
    m_rightEncoder = m_subsystem.getRightEncoder();

    // Use addRequirements() here to declare subsystem dependencies.
    addRequirements(m_subsystem);
  }

Now change the line in the RobotContainer to turn the robot by 180 degrees:

    m_button5.whenPressed(new TurnCommand(m_driveSubsystem, 0.5, 180));

Before moving on, verify that if you specify a negative angle the robot will turn the other way:

    m_button5.whenPressed(new TurnCommand(m_driveSubsystem, 0.5, -180));

You may find that the robot does not turn exactly the desired 180 degrees. This is because the accuracy of the turns using this method is poor because of wheel slippage and other issues. If the robot had a gyroscope, we could use that for more accurate turns, but since it does not, we will need to live with the inaccuracies.

Next: Combining Commands