Commands

beginner30 min

Overview

Commands define robot actions. They use subsystem methods to perform tasks, and the scheduler decides which ones run.

Command Lifecycle

Four key methods:

  • initialize(): Called once when command starts.
  • execute(): Called every loop (20ms) while scheduled.
  • isFinished(): Returns true when command should end.
  • end(interrupted): Called once when command ends or is interrupted.

Subsystem Command Factories

For an action that involves a single subsystem, the shortest correct version is a method on that subsystem. SubsystemBase provides factory methods that build a command already requiring this:

public class Intake extends SubsystemBase {
    // Runs every loop while scheduled
    public Command setIntakeCommand(double speed) {
        return run(() -> setIntakeDutyCycle(speed));
    }

    // Runs once, then finishes
    public Command setIntakePosCommand(double pos) {
        return runOnce(() -> setIntakePosition(pos));
    }
}

The factory methods on SubsystemBase

MethodBehaviourEnds when
run(Runnable)Calls the runnable every loopNever — runs until interrupted
runOnce(Runnable)Calls the runnable onceImmediately
startEnd(onStart, onEnd)One action at start, another at endWhen interrupted
runEnd(whileRunning, onEnd)Every loop, plus cleanupWhen interrupted

Which one you want follows from what the hardware needs. A duty cycle must be re-commanded every loop or the motor safety watchdog cuts it off, so use run(). A closed-loop setpoint is held by the motor controller itself, so runOnce() is enough — re-sending it 50 times a second achieves nothing.

Requirements come free

run and runOnce are instance methods on the subsystem, so the command they return already requires that subsystem. There is no addRequirements() call to forget.

That matters more than it sounds. A command with no requirements conflicts with nothing, so two of them can drive the same motor at once with no error and no symptom beyond a mechanism that behaves strangely.

Because a factory is a method, it can take arguments — one method covers what would otherwise be several classes:

intake.setIntakeCommand(1)      // intake
intake.setIntakeCommand(-1)     // eject
intake.setIntakeCommand(0)      // stop

The alternative is RunIntakeCommand, EjectIntakeCommand, and StopIntakeCommand, all nearly identical. Bindings read better too: intake.deployAndIntakeCommand() says what happens once, where new DeployAndIntakeCommand(intake) says it twice.

When a setpoint has a meaning, name the factory after the meaning rather than parameterizing it:

public Command intakeDeployCommand() {
    return runOnce(() -> setIntakePosition(INTAKE_PIVOT_DOWN));
}

public Command intakeHomeCommand() {
    return runOnce(() -> setIntakePosition(INTAKE_PIVOT_UP));
}

Inline Commands

For a one-off action that does not deserve a factory, Commands has static builders:

import edu.wpi.first.wpilibj2.command.Commands;

// Run continuously while scheduled
Command runCmd = Commands.run(() -> intake.setIntakeDutyCycle(0.6), intake);

// Run once immediately, then finish
Command stopCmd = Commands.runOnce(() -> intake.setIntakeDutyCycle(0), intake);

// Run until a condition is met
Command untilCmd = intake.setIntakeCommand(0.6).until(intake::hasGamePiece);

Note the subsystem passed as the second argument — with the static form, requirements are not automatic.

Full Command Classes

Write a class extending Command when an action coordinates several subsystems that must react to each other, every loop. A factory cannot express that, because run and runOnce require exactly one subsystem.

Shooting while moving is the standard example: the drive pose determines the flywheel speed, the flywheel speed determines whether the feeder may run, and all of it is recomputed continuously.

package frc.robot.commands;

public class SmartShootCommand extends Command {
    private final Shooter shooter;
    private final Transfer transfer;
    private final Conveyor conveyor;
    private final Drive drive;

    public SmartShootCommand(Drive drive, Shooter shooter, Transfer transfer, Conveyor conveyor) {
        this.shooter = shooter;
        this.transfer = transfer;
        this.conveyor = conveyor;
        this.drive = drive;
        addRequirements(shooter, transfer, conveyor);
    }

    @Override
    public void initialize() {
        transfer.feeding = false;
    }

    @Override
    public void execute() {
        double distance = drive.getPose().getTranslation().getDistance(targetPose.getTranslation());
        double setpoint = shooter.getVelo(distance);
        shooter.setPIDSetpoint(setpoint);

        Logger.recordOutput(getName() + "/distance", distance);
        Logger.recordOutput(getName() + "/setpoint", setpoint);

        if (shooter.atOrAboveSpeed()) {
            transfer.setTransferDutyCycle(1.0);
            conveyor.setConveyorDutyCycle(-1.0);
        }
    }

    @Override
    public void end(boolean interrupted) {
        transfer.setTransferDutyCycle(0);
        conveyor.setConveyorDutyCycle(0);
        shooter.setDutyCycleSetpoint(0);
    }

    @Override
    public boolean isFinished() {
        return false;
    }
}

Requirements

Commands declare subsystem requirements to prevent conflicts. The scheduler ensures only one command controls a subsystem at a time; if a new command requires a subsystem in use, it interrupts the current one.

Require what you command, not what you read

Four subsystems are passed into the constructor above. Only three are required.

drive is deliberately left out, because the command only reads the drive pose — it never commands the drivetrain. Requiring it would interrupt the joystick drive, and the driver would lose control the instant they pressed the shoot button.

Over-requiring is the more common mistake and the harder one to debug: the mechanism works, but pressing the button silently cancels something else.

Ending Cleanly

isFinished() returning false means the command runs until something cancels it — correct for a whileTrue binding, where the trigger controls its lifetime.

That makes end() load-bearing. It runs on interruption as well as normal completion, and it must stop everything the command started. Default commands only cover subsystems this command required; anything else left commanded here stays commanded.

Logging From a Command

Logger.recordOutput(getName() + "/distance", distance);

getName() returns the command’s class name, so keys come out as SmartShootCommand/distance and follow the class if it is ever renamed.

Log the intermediate values, not just the final output. When a shot misses, distance and setpoint are what tell you whether the pose estimate was wrong or the shot table was.

Which form to use

SituationWrite
One subsystemA factory method on that subsystem
Several subsystems, independent of each otherCompose factories — see Command Groups
Several subsystems that must react to each otherA class extending Command

Common mistakes

  • A run() factory for a closed-loop setpoint, flooding the bus for no reason.
  • A runOnce() factory for a duty cycle, which motor safety will cut off.
  • Requiring a subsystem you only read from.
  • An end() that does not stop everything it started.
  • State initialised in the constructor instead of initialize(). The constructor runs once at startup; the command may be scheduled hundreds of times.
  • Blocking loops in execute(). Everything runs on one thread — a loop that waits freezes the whole robot.
  • Reaching for a class too early. If the action fits one subsystem, it is a factory.

Resources