Unit 6 · Lesson 4

Creating Basic Commands

Commands are the verbs of your robot. You have the nouns (subsystems) — now you write the actions. Every command has the same four lifecycle methods, and understanding exactly when each one runs is the foundation of reliable Command-Based code.

By the end of this lesson, you will:

  • Implement all four command lifecycle methods: initialize(), execute(), isFinished(), and end(boolean interrupted)
  • Declare subsystem requirements with addRequirements() and explain why this is non-negotiable
  • Write a DriveWithJoysticksCommand that reads controller input and drives a subsystem
  • Write an IntakeCommand that uses a sensor to determine when to finish
  • Explain the difference between a command that ends on its own vs. one that runs indefinitely
  • Always call subsystem.stop() in end() and explain why omitting it is a safety bug

The Four Lifecycle Methods

Every command class has the same four methods. The scheduler calls them in a fixed sequence. You override the ones that need work — the rest can stay empty (but you should still understand why they exist). Click each method below to see what it does and when.

Command Lifecycle — click a method to learn when it runs
initialize()
Called once — when the command starts
execute()
Called every 20 ms while running
isFinished()
Called every 20 ms — return true to end
end(boolean interrupted)
Called once — when the command stops
← select a method to see its purpose, rules, and what to put in it

Example 1: DriveWithJoysticksCommand

The default drive command is the first command every team writes. It has no end condition — it runs continuously as the default command on DriveSubsystem. The driver's stick values flow through it every 20 ms. Click the highlighted tokens.

DriveWithJoysticksCommand.java
public class DriveWithJoysticksCommand extends CommandBase {

  private final DriveSubsystem m_drive;
  private final XboxController m_controller;

  public DriveWithJoysticksCommand(DriveSubsystem drive, XboxController controller) {
    m_drive = drive;
    m_controller = controller;
    addRequirements(m_drive);
  }

  @Override
  public void initialize() {
    // Nothing to set up — we just start reading the stick
  }

  @Override
  public void execute() {
    double speed = MathUtil.applyDeadband(
      -m_controller.getLeftY(), Constants.kDriveDeadband);
    double turn = MathUtil.applyDeadband(
      m_controller.getRightX(), Constants.kDriveDeadband);
    m_drive.arcadeDrive(speed, turn);
  }

  @Override
  public boolean isFinished() {
    return false; // default command — runs forever
  }

  @Override
  public void end(boolean interrupted) {
    m_drive.stop();
  }
}
← click a highlighted token

Example 2: IntakeCommand

The intake command has a real end condition — it finishes when the beam break detects a game piece. This is a sensor-terminated command: it starts on a button press, runs until it succeeds, then cleans up automatically. This pattern replaces the state machine from Unit 5.

IntakeCommand.java
public class IntakeCommand extends CommandBase {

  private final IntakeSubsystem m_intake;

  public IntakeCommand(IntakeSubsystem intake) {
    m_intake = intake;
    addRequirements(m_intake);
  }

  @Override
  public void initialize() {
    // Spin up rollers immediately on command start
    m_intake.setSpeed(Constants.kIntakeSpeed);
  }

  @Override
  public void execute() {
    // Motor is already running — nothing to repeat here
    // If speed needed PID correction we'd update it here
  }

  @Override
  public boolean isFinished() {
    // Stop when the beam break fires — game piece is loaded
    return m_intake.hasGamePiece();
  }

  @Override
  public void end(boolean interrupted) {
    m_intake.stop(); // Always stop, whether finished or interrupted
  }
}
← click a highlighted token
⚠️ Always stop in end() — always

This is the most common safety omission in Command-Based code. If your command's end() method does not call m_subsystem.stop(), the motor keeps running at whatever speed execute() last commanded. When the command ends, the subsystem is freed and the default command takes over — but the motor is already spinning. On a drivetrain this means the robot continues moving. On an intake this means pieces get jammed. On an arm this means a mechanism in motion with no active control. One line. Every time. No exceptions.

🔍 LRI Perspective: "Commands vs. state machines — the right tool for the job"

The intake command above replaces the state machine from Unit 5, Lesson 12. The difference: the Unit 5 state machine tracked state manually across loop iterations using a field. This command delegates that to the scheduler — the scheduler tracks "is this command running" so you don't have to. The pattern choice matters: use commands for actions that have a clear start, middle, and end. Use state machines (inside a command's execute()) for mechanisms that need complex internal state, like a multi-mode shooter that can be in IDLE, SPINNING_UP, or READY states regardless of what the driver is doing.

The Constructor Pattern: Passing Dependencies

Commands receive their subsystem dependencies through the constructor. This is dependency injection — the command doesn't create its own subsystem instances, it receives them. This means the same IntakeCommand can be used by multiple triggers, in autonomous command groups, and in simulation, all pointing to the same subsystem instance.

In RobotContainer, the pattern looks like this:

RobotContainer.java — wiring subsystems to commands
public class RobotContainer {

  private final DriveSubsystem m_drive = new DriveSubsystem();
  private final IntakeSubsystem m_intake = new IntakeSubsystem();
  private final XboxController m_driver = new XboxController(0);
  private final XboxController m_operator= new XboxController(1);

  public RobotContainer() {
    // Default drive command receives subsystem + controller
    m_drive.setDefaultCommand(
      new DriveWithJoysticksCommand(m_drive, m_driver)
    );
    configureButtonBindings();
  }

  private void configureButtonBindings() {
    // A button held → run IntakeCommand on m_intake
    new JoystickButton(m_operator, XboxController.Button.kA.value)
      .whileTrue(new IntakeCommand(m_intake));
  }
}

The subsystem instances are created once in RobotContainer. They're passed as arguments to every command that uses them. The command holds a reference, not a copy — so there is always exactly one IntakeSubsystem object, and all commands that touch the intake operate on the same hardware state.

🔌 System Check — First Command Deployment

Before enabling with your first commands attached:

  • Verify addRequirements() is called in every command constructor. A command without requirements runs without subsystem ownership — two commands can control the same hardware simultaneously. Open the Scheduler widget in Shuffleboard and confirm the command name appears when the trigger fires. If it doesn't appear, the command was rejected (subsystem conflict) or the trigger wasn't registered properly.
  • Test end() before execute(). Before testing full command behavior, verify the stop logic: schedule the command, immediately cancel it (command.cancel()), and confirm the motor stops. If it doesn't, your end() method is missing a stop() call.
  • The robot must be on blocks for first command testing. A command that runs arcadeDrive(1.0, 0.0) immediately on initialization will drive the robot at full speed. Blocks prevent the robot from leaving the test area.

Knowledge Check

1. Your IntakeCommand runs, the beam break fires, isFinished() returns true, and the command ends. But the intake rollers keep spinning. What did you forget?

  • A isFinished() should call m_intake.stop() before returning true.
  • B The end(boolean interrupted) method is missing a call to m_intake.stop(). When a command ends (for any reason), end() is called — that's the cleanup point. If it doesn't stop the motor, the motor stays at its last commanded speed.
  • C initialize() needs to call m_intake.stop() first to reset the motor before starting.
  • D The scheduler automatically stops all motors when a command ends — no end() code is needed.

2. You forget to call addRequirements(m_drive) in DriveWithJoysticksCommand. What is the most dangerous consequence?

  • A The command will throw a NullPointerException when execute() runs.
  • B The command will not compile — addRequirements() is a required method call.
  • C Another command requiring DriveSubsystem will not interrupt DriveWithJoysticksCommand — both will run simultaneously, fighting over the drivetrain motors. The scheduler only enforces ownership if requirements are declared.
  • D The default command won't be scheduled — without requirements, the scheduler doesn't know which subsystem to assign it to.

3. Your command's execute() starts a motor at full speed. isFinished() depends on a sensor that never fires (it's broken). What happens to the robot?

  • A The command throws a timeout exception after 30 seconds.
  • B The command runs forever, holding the subsystem and running the motor continuously — until the operator disables the robot or a competing command interrupts it. There is no automatic timeout unless you explicitly add one.
  • C The scheduler detects the sensor failure and cancels the command automatically.
  • D The command exits after execute() completes its first iteration.
💪 Practice Prompt

Build DriveWithJoysticks and IntakeCommand

  1. Create DriveWithJoysticksCommand.java. Pass DriveSubsystem and XboxController via the constructor. Implement execute() with deadband using MathUtil.applyDeadband() and Constants.kDriveDeadband. Return false from isFinished(). Call m_drive.stop() in end(). Set it as the default command in RobotContainer. Deploy and drive.
  2. Create IntakeCommand.java. In initialize(), start the intake motor at Constants.kIntakeSpeed. In isFinished(), return m_intake.hasGamePiece(). In end(), call m_intake.stop(). Bind it to the A button with .whileTrue() in RobotContainer. Test: hold A, insert a simulated game piece (block the beam break by hand), confirm the intake stops and releases the button press.
  3. Create an EjectCommand: runs the intake in reverse at a configurable speed for 0.5 seconds using a Timer field. initialize() starts the timer and starts the motor. isFinished() returns m_ejectTimer.hasElapsed(0.5). end() stops the motor. Bind to B button with .onTrue(). Confirm in Shuffleboard that the command name disappears after 0.5 seconds.
  4. Stretch goal: Deliberately break the beam break simulation (comment out the sensor read, hardcode hasGamePiece() to return false). Hold the A button. Observe the Scheduler widget — the IntakeCommand runs indefinitely. This is the sensor failure scenario from Quiz Q3 in a controlled setting. Now add a timeout using WPILib's .withTimeout(3.0) decorator on the command binding in RobotContainer: .whileTrue(new IntakeCommand(m_intake).withTimeout(3.0)). The command now terminates after 3 seconds even if the sensor never fires.