dev only

Commands & Command Compositions

The Four Lifecycle Methods

A Command defines what it does through four overridable methods, all of which default to doing nothing (or false) so you only override the ones you actually need:

  • initialize() — called exactly once, when the command starts. Use it to set the command's starting state.
  • execute() — called repeatedly, once per scheduler run, for as long as the command is scheduled. Use it for anything that needs to happen continuously (following joystick input, running a control loop).
  • isFinished() — called every loop, after execute(). As soon as it returns true, the command ends. Defaults to false, meaning a command runs forever unless something else interrupts it.
  • end(boolean interrupted) — called exactly once, when the command stops, whether it finished naturally or was interrupted. The interrupted flag distinguishes the two cases. Use it to clean up (stop a motor, reset a flag).
public class GrabHatch extends Command
{
    private final HatchSubsystem hatchSubsystem;

    public GrabHatch(HatchSubsystem subsystem)
    {
        hatchSubsystem = subsystem;
        addRequirements(hatchSubsystem); // declares the resource this command needs
    }

    @Override
    public void initialize() { hatchSubsystem.grabHatch(); }

    @Override
    public boolean isFinished() { return true; } // runs once, then immediately ends
}

Notice the subsystem is passed into the command's constructor — this is dependency injection (a pattern you'll see again in Lesson 25.7), and it's what lets a command reach a subsystem's methods without that subsystem being a global variable anywhere.

Requirements: The Resource-Management Backbone

Every command declares which subsystems it needs, via addRequirements(...). This is what backs the scheduler's promise that no two commands ever fight over the same subsystem at once (Lesson 25.3 covers exactly how conflicts get resolved).

Built-In Command Factories: You Rarely Need a Custom Class

Writing a full Command subclass (like GrabHatch above) for every simple action is unnecessarily verbose. The library provides factory methods, built on lambdas, that cover almost every real case:

Commands.runOnce(() -> hatchSolenoid.set(kForward), hatchSubsystem);       // runs a lambda once, then finishes
Commands.run(() -> drive.arcadeDrive(joystick.getY(), joystick.getX()), drive); // repeats until interrupted — a natural default command
Commands.startEnd(
    () -> shooter.setSpeed(0.5),  // runs once at the start
    () -> shooter.setSpeed(0.0),  // runs once when the command ends
    shooter
);
Commands.waitSeconds(5.0);       // finishes 5 seconds after being scheduled
Commands.waitUntil(limitSwitch::get); // finishes once the given condition becomes true

FunctionalCommand covers the general case — four lambdas, one for each of the four lifecycle methods — for when a command needs real custom behavior in more than one of them.

Command Compositions: Building Bigger Actions From Smaller Ones

A command composition combines several commands into one — and since a composition is itself a Command, compositions can be nested arbitrarily deep. Three shapes matter most:

  • Sequential — runs each command in order, one after another, finishing when the last one finishes:
    fooCommand.andThen(barCommand); // runs fooCommand, then barCommand
    
  • Parallel — runs several commands at once, finishing once all of them finish:
    Commands.parallel(twoSecCommand, oneSecCommand, threeSecCommand); // finishes after 3 seconds
    
  • Race — runs several commands at once, finishing as soon as any one of them finishes (interrupting the rest):
    Commands.race(twoSecCommand, oneSecCommand, threeSecCommand); // finishes after 1 second
    

Two useful decorators add extra end conditions to any command:

command.until(limitSwitch::get); // ends early if the limit switch trips
command.withTimeout(5);          // ends after 5 seconds no matter what

A composition inherits the union of every component's requirements — a sequence that uses the intake, then the indexer, then the shooter reserves all three subsystems for the composition's entire duration, not just while each is actively in use.

Common Pitfalls

  • Scheduling a command that's already inside a composition. Once a command is part of a composition, it can't also be independently scheduled — attempting to do so throws and crashes the program, since the same command instance would be running from two places at once.
  • Writing a full custom Command subclass for something a factory already covers. The built-in factories (runOnce, run, startEnd, waitUntil, etc.) cover the overwhelming majority of real cases with far less code.
  • Forgetting that a sequential composition needs every step to actually finish. If one command in a sequence never calls its isFinished() true, every command after it in the sequence never runs.

Key Takeaways

  • A Command's behavior is defined by four lifecycle methods: initialize(), execute(), isFinished(), end(interrupted) — all default to reasonable no-ops.
  • addRequirements(...) declares which subsystems a command needs, backing the scheduler's resource-conflict prevention.
  • Built-in factories (runOnce, run, startEnd, waitSeconds, waitUntil, FunctionalCommand) cover almost every use case without writing a custom class.
  • Compositions (sequential, parallel, race) combine commands into bigger ones, and are themselves ordinary commands — usable recursively, nested as deep as needed.

Derived from other-reference-repo: wpilib/command-based/02-commands.md, 03-command-compositions.md (WPILIB 20.2-3)