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(), andend(boolean interrupted) - Declare subsystem requirements with
addRequirements()and explain why this is non-negotiable - Write a
DriveWithJoysticksCommandthat reads controller input and drives a subsystem - Write an
IntakeCommandthat 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()inend()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.
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.
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();
}
}
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.
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
}
}
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.
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:
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.
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()beforeexecute(). 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, yourend()method is missing astop()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?
2. You forget to call addRequirements(m_drive) in DriveWithJoysticksCommand. What is the most dangerous consequence?
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?
Build DriveWithJoysticks and IntakeCommand
- Create
DriveWithJoysticksCommand.java. PassDriveSubsystemandXboxControllervia the constructor. Implementexecute()with deadband usingMathUtil.applyDeadband()andConstants.kDriveDeadband. ReturnfalsefromisFinished(). Callm_drive.stop()inend(). Set it as the default command inRobotContainer. Deploy and drive. - Create
IntakeCommand.java. Ininitialize(), start the intake motor atConstants.kIntakeSpeed. InisFinished(), returnm_intake.hasGamePiece(). Inend(), callm_intake.stop(). Bind it to the A button with.whileTrue()inRobotContainer. Test: hold A, insert a simulated game piece (block the beam break by hand), confirm the intake stops and releases the button press. - Create an
EjectCommand: runs the intake in reverse at a configurable speed for 0.5 seconds using aTimerfield.initialize()starts the timer and starts the motor.isFinished()returnsm_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. - Stretch goal: Deliberately break the beam break simulation (comment out the sensor read, hardcode
hasGamePiece()to returnfalse). 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.