Combining Commands

Now that we have commands to drive and turn let’s see how we can combine them to create more complex commands. Let’s create a new command that will drive the robot around a certain path.  The first step is to create a new command called DriveCourseCommand. This type of command has a different structure so this time instead of copying the ExampleCommand we are going to make this new class inherit from SequentialCommandGroup instead of CommandBase.

To create the new class, right click on the commands folder, choose New File, and enter the name DriverCourseCommand.java. Then replace the text in that file with:

package robot.commands;

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

public class DriveCourseCommand extends SequentialCommandGroup {
  /**
   * Creates a new NewCommand.
   */
  private final DriveSubsystem m_subsystem;

  public DriveCourseCommand(DriveSubsystem subsystem) {
    Logger.Log("DriveCourseCommand", 3, "DriveCourseCommand()");

    m_subsystem = subsystem;
  }
}

Note that for this type of command, we inherit from SequentialCommandGroup. Also we do not have the normal command functions (e.g. initialize, execute, etc.). This is because this command will be composed of a set of composite commands.

For example, if we want to drive forward 20 inches and then turn right by 90 degrees, we would add the following lines to the constructor:

  public DriveCourseCommand(DriveSubsystem subsystem) {
    Logger.Log("DriveCourseCommand", 3, "DriveCourseCommand()");

    m_subsystem = subsystem;

    double turn90 = 70;
    double speed = 0.5;
    double wait = 0.5;

    
    addCommands(  new DriveForDistanceCommand(m_subsystem, speed, distance),
                  new WaitCommand(wait),
                  new TurnCommand(m_subsystem, speed, turn90));
  }

Notice how I placed the built in WaitCommand between the DriveForDistanceCommand and TurnCommand. The WaitCommand does nothing but wait for a specified time. Adding a small wait between the commands will increase the accuracy of the turns. Transitioning directly from driving to turning can cause errors like wheel slippage which will throw of our turn calculations. I am also using variables turn90, speed, and wait rather than specifying the numbers directly in the respective constructors. I am doing this since we will be adding multiple commands that use these numbers and defining them all in one place will make it easier for us to adjust the numbers if needed.

Now modify the RobotContainer class so that we run this new command when we press our test button 6.

/*----------------------------------------------------------------------------*/
/* 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.DriveCourseCommand;
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 JoystickButton m_button6 = new JoystickButton(m_joystick, 6);

  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, -180));
    m_button6.whenPressed(new DriveCourseCommand(m_driveSubsystem));
  }

  /**
   * 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 verify that it drives forward 20 inches, turns right and then stops.

Now let’s change your program so that the robot will drive in a rectangular pattern 20 inches by 10 inches. In principle this should bring your robot back to the starting position. After you have completed and tested your code, compare it to my solution below.

.

.

.

.

.

.

package robot.commands;

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

public class DriveCourseCommand extends SequentialCommandGroup {
  /**
   * Creates a new NewCommand.
   */
  private final DriveSubsystem m_subsystem;

  public DriveCourseCommand(DriveSubsystem subsystem) {
    Logger.Log("DriveCourseCommand", 3, "DriveCourseCommand()");

    m_subsystem = subsystem;

    double turn90 = 80;
    double speed = 0.5;
    double wait = 0.5;
    
    addCommands(  new DriveForDistanceCommand(m_subsystem, speed, 20),
                  new WaitCommand(wait),
                  new TurnCommand(m_subsystem, speed, turn90),
                  new WaitCommand(wait),
                  new DriveForDistanceCommand(m_subsystem, speed, 10),
                  new WaitCommand(wait),
                  new TurnCommand(m_subsystem, speed, turn90),
                  new WaitCommand(wait),
                  new DriveForDistanceCommand(m_subsystem, speed, 20),
                  new WaitCommand(wait),
                  new TurnCommand(m_subsystem, speed, turn90),
                  new WaitCommand(wait),
                  new DriveForDistanceCommand(m_subsystem, speed, 10),
                  new WaitCommand(wait),
                  new TurnCommand(m_subsystem, speed, turn90));
  }
}

Note that in my case I needed set the turn90 to 80 degrees instead of 90 in order to get the robot to drive the correct path. You will probably find that it was very hard to set the speed and turn parameters correctly to bring the robot back to the exact starting position. In fact, you probably saw that running the same program twice in a row actually produced a slightly different outcome. This is because there are a number of things that go on which introduce slight errors (such as wheel slippage), and these errors accumulate over time and are different every time. You might find that if you slow the robot down, you can get more reproducible results, but you can never reach the point where it is always spot on.

The important thing to take away from this is that this kind of ‘dead reckoning’ navigation is fraught with problems. The only way to overcome these problems is to use sensors (e.g. gyro, distance sensor, camera, etc.) on the robot that give you a way to determine it’s absolute position and orientation by observing and using the environment around it.

Next: Using Sensors