498
Control Lab FRC Programming Curriculum
Software Engineering · L03 of 8

Command Composition

Prereqs: software-engineering-01
Objectives 0 / 4

Hook

Autonomous mode. Fifteen seconds. Your routine needs to:

  1. Drive forward 1 meter.
  2. Wait for the arm to reach scoring position.
  3. Run the shooter for exactly 1.5 seconds.
  4. Retract the arm and drive backward simultaneously.
  5. Stop everything when the drive encoder hits 2 meters.

In monolithic code this is a hundred-line state machine with flags for each step, edge cases for interruptions, and zero reuse — you rewrite it from scratch for every auto variant.

With command composition, it’s twelve lines of readable Java. Each step is a reusable command. The composition is declarative: you describe the shape of the routine, and the scheduler runs it.

Core concept

Key Concept

Command composition builds complex behaviors from simple, reusable command primitives. Sequential groups run commands one after another. Parallel groups run commands simultaneously. Race and Deadline groups add completion policies. Composing small, tested pieces is faster and safer than writing monolithic auto routines.

The composition types

SequentialCommandGroup

Runs commands one at a time, in order. The next command starts only after the previous one finishes.

new SequentialCommandGroup(
    new DriveForwardCommand(drive, 1.0),   // runs first
    new ArmToScoringCommand(arm),           // runs second, after drive finishes
    new ShootCommand(shooter)               // runs third
);

When to use: Multi-step sequences where order matters and each step must complete before the next begins.

ParallelCommandGroup

Runs all commands simultaneously. The group finishes when all commands finish.

new ParallelCommandGroup(
    new ArmRetractCommand(arm),    // runs at the same time
    new DriveBackwardCommand(drive, 1.0)  // both must finish
);

When to use: Independent mechanisms that can move at the same time without interfering.

⚠ Heads up

Never put two commands that require the same subsystem in a ParallelCommandGroup. The scheduler will throw a runtime exception — requirement conflicts inside a group are illegal.

ParallelRaceGroup

Runs all commands simultaneously. The group finishes when any one command finishes — the rest are interrupted.

new ParallelRaceGroup(
    new ShootCommand(shooter),          // run shooter...
    new WaitCommand(1.5)                // ...but stop after 1.5 seconds regardless
);

When to use: When you want a command to run “for at most X seconds” or “until a condition fires.”

ParallelDeadlineGroup

Runs all commands simultaneously. The group finishes when one specific “deadline” command finishes — others are interrupted. The deadline is the first argument.

new ParallelDeadlineGroup(
    new DriveToZoneCommand(drive),       // deadline — everything stops when drive finishes
    new IntakeSpinCommand(intake),       // runs alongside drive, cancelled when drive ends
    new LEDFlashCommand(leds)            // also runs alongside, also cancelled
);

When to use: One command defines “done” for the whole group; others are support/visual/feedback that should run during it.

Factory methods: Commands.sequence() and Commands.parallel()

Since WPILib 2023, you can use static factory methods instead of new SequentialCommandGroup(...). They’re shorter and compose more naturally inline:

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

Command auto = sequence(
    runOnce(() -> drive.resetEncoders(), drive),
    parallel(
        new ArmToPositionCommand(arm, 45.0),
        new WaitCommand(0.5)              // give drive time to stabilize
    ),
    run(() -> shooter.setRPM(4500), shooter).withTimeout(2.0),
    parallel(
        new ArmRetractCommand(arm),
        new DriveBackwardCommand(drive, 1.5)
    )
);

Commands.run() wraps a lambda as a command. Commands.runOnce() wraps a lambda that runs exactly once. Both require you to pass the subsystems they use so the scheduler can enforce requirements.

WaitCommand and WaitUntilCommand

WaitCommand(seconds) — does nothing for a fixed duration, then finishes. Use it to add pauses between steps.

sequence(
    new ShootCommand(shooter),
    new WaitCommand(0.3),           // wait 300ms for game piece to clear
    new IntakeCommand(intake)
);

WaitUntilCommand(BooleanSupplier) — waits until a condition becomes true. The condition is polled every loop.

sequence(
    new ArmToPositionCommand(arm, 90.0),
    new WaitUntilCommand(arm::atTarget),   // don't proceed until arm is in position
    new ShootCommand(shooter)
);
Note

WaitUntilCommand is strictly better than WaitCommand for sensor-gated sequences. A fixed wait assumes timing; WaitUntilCommand responds to reality. Prefer it whenever a sensor or subsystem state is available.

Inline command shortcuts

Several common patterns have one-line shorthand:

// Run a lambda as a command (repeats each execute loop)
Commands.run(() -> arm.setTargetAngle(90.0), arm)

// Run a lambda exactly once
Commands.runOnce(() -> drive.resetEncoders(), drive)

// Run a command, then stop after N seconds
new ArmCommand(arm).withTimeout(3.0)

// Add a condition that cancels the command if it becomes true
new DriveCommand(drive).until(drive::atTarget)

// Print something for debugging (no requirements)
Commands.print("Reached waypoint 1")

Building a complete auto routine

Here is a full 5-step auto routine using composition:

public Command getAutonomousCommand() {
    return sequence(
        // Step 1: Reset sensors
        runOnce(() -> {
            drive.resetEncoders();
            arm.resetPosition();
        }, drive, arm),

        // Step 2: Drive forward and raise arm at the same time
        parallel(
            new DriveToDistanceCommand(drive, 1.0),
            new ArmToPositionCommand(arm, 120.0)
        ),

        // Step 3: Wait until the arm is actually at position (sensor-gated)
        new WaitUntilCommand(arm::atTarget),

        // Step 4: Shoot for up to 1.5 seconds, or until piece is gone
        race(
            new ShootCommand(shooter),
            new WaitCommand(1.5)
        ),

        // Step 5: Retract and back up simultaneously
        parallel(
            new ArmToPositionCommand(arm, 0.0),
            new DriveToDistanceCommand(drive, -0.5)
        )
    );
}

This is every step readable at a glance. Changing the arm target angle for a different scoring position is a one-number change. Swapping step 4 to a different game element takes one line.

Reusing commands across auto variants

The real power is reuse. Build named command factories in RobotContainer or an Autos class:

public class Autos {
    public static Command scoreLow(ArmSubsystem arm, ShooterSubsystem shooter) {
        return sequence(
            new ArmToPositionCommand(arm, 45.0),
            new WaitUntilCommand(arm::atTarget),
            new ShootCommand(shooter).withTimeout(1.0)
        );
    }

    public static Command scoreHigh(ArmSubsystem arm, ShooterSubsystem shooter) {
        return sequence(
            new ArmToPositionCommand(arm, 120.0),
            new WaitUntilCommand(arm::atTarget),
            run(() -> shooter.setRPM(5000), shooter).withTimeout(1.5)
        );
    }

    public static Command twoGamePieceAuto(
            DriveSubsystem drive,
            ArmSubsystem arm,
            ShooterSubsystem shooter,
            IntakeSubsystem intake) {
        return sequence(
            scoreLow(arm, shooter),
            parallel(
                new DriveToDistanceCommand(drive, 2.0),
                sequence(
                    new WaitCommand(0.5),         // arm clear before intake
                    new IntakeCommand(intake).withTimeout(2.0)
                )
            ),
            new WaitUntilCommand(intake::hasPiece),
            scoreHigh(arm, shooter)
        );
    }
}
Note

Static factory methods that return Command objects are the cleanest way to organize autonomous routines. They compose naturally, are easy to unit-test, and keep RobotContainer readable.

Key takeaways

  • SequentialCommandGroup — one at a time, in order, waits for each to finish.
  • ParallelCommandGroup — all at once, finishes when all finish.
  • ParallelRaceGroup — all at once, finishes when any one finishes; others are interrupted.
  • ParallelDeadlineGroup — all at once, finishes when the first (deadline) command finishes.
  • Commands.sequence() and Commands.parallel() are cleaner factory alternatives to new XxxCommandGroup(...).
  • WaitUntilCommand is always better than WaitCommand when a sensor or state is available.
  • Build named factory methods — compose those in autonomous routines for readability and reuse.

Common confusions

“My parallel group throws ‘CommandScheduler: Commands in parallel group share requirements’.” Two commands in the group require the same subsystem. Split them into a sequential group, or redesign so the subsystem is only used by one command at a time.

“WaitUntilCommand is waiting forever.” The BooleanSupplier is never returning true. Add telemetry: Commands.print("atTarget: " + arm.atTarget()) before the wait step to debug the condition.

“I used race() but the command that ‘wins’ is wrong.” race() ends when any command finishes — including WaitCommand. If the timer fires first, the other commands are interrupted. Use deadline() if one specific command should be the termination condition.

“My sequence skips step 2.” Check that step 1’s isFinished() actually returns true. If it immediately returns true (e.g., encoder is already at zero when the command starts and you forgot to reset it), step 2 starts and ends before you notice.

Challenge

⚡ Try it yourself

Design a command composition for this 3-game-piece autonomous routine:

  1. Drive forward 0.5 m while running the intake simultaneously.
  2. Wait until the intake confirms it has a piece.
  3. Score the piece (drive 1.5 m forward, then shoot for up to 1 s).
  4. Drive back to the starting zone (1.5 m backward).
  5. Repeat steps 1-2 for a second piece, then score it.

What composition type does each step use? Where does WaitUntilCommand appear? What commands could be extracted into reusable factory methods?

Code EditorJavaCtrl+Enter to run
Stuck? Show hint

Call runSequence(driveForward) first, then runRace(shootCmd, timer), then runSequence(driveBack). Each function handles one step.

What’s next

In Software Engineering Lesson 04, we’ll cover the Advanced Trigger System — how to build reactive, event-driven bindings from sensors and computed conditions, without any polling code in periodic().