Gray Matter
WorkshopWriting Commands
WPILib 2027 is still in alpha: these pages change as the APIs settle.
LESSON 12

Writing Commands

The voltage setter is private and nothing can reach it. On branch mech-2-Commands three commands become the way in: one slow, one fast, one that holds the mechanism still.

Branchmech-2-Commands7 minutes
You’ll need
  • Branch mech-1-Mechanisms building clean, with a private setVoltage and stopMotor on Arm and Flywheel.
  • Lambdas, method references, and private from Java Basics.
  • robot.arm and robot.flywheel from Mechanisms. The bindings below reach the mechanisms through that object.

What mechanism are you working on?

The lesson below is written for the one you pick. Switch back any time to read it for the other.

The armflywheel has a working motor and no way yet to ask it for anything. A command is the way in, and it wraps that private setter. The scheduler hands the mechanism to one command at a time, so two of them can never fight over the same motor.

One import

Open src/main/java/first/robot/mechanisms/Arm.javasrc/main/java/first/robot/mechanisms/Flywheel.java. One line goes in at the top, alphabetically, just above Mechanism.

the bottom of the import block
import com.ctre.phoenix6.signals.NeutralModeValue;
import org.wpilib.command3.Command;
import org.wpilib.command3.Mechanism;

That is the only edit to what is already in the file. setVoltage is already private, and the stop helper is already called stopMotor, so the command below can take the name stop without displacing anything.

Three commands

Three methods, and they all look alike.

Arm.java: the three commands
// Each command uses runRepeatedly, which runs its action every loop while the
// command is scheduled. Every one of them is a hold: it never finishes on its own.
 
/** Push the arm at 3 volts and keep pushing. Never finishes. */
public Command runSlow() {
return runRepeatedly(() -> setVoltage(3.0)).named("runSlow (hold)");
}
 
/** Push the arm at 6 volts and keep pushing. Never finishes. */
public Command runFast() {
return runRepeatedly(() -> setVoltage(6.0)).named("runFast (hold)");
}
 
/** Stop the arm motor and keep it stopped. Never finishes. */
public Command stop() {
return runRepeatedly(this::stopMotor).named("stop (hold)");
}
Flywheel.java: the three commands
// Same setup as Arm. runRepeatedly runs the action every loop while the command
// is scheduled. Every one of these is a hold: it never finishes on its own.
 
/** Spin the flywheel at 3 volts and hold it there. Never finishes. */
public Command runSlow() {
return runRepeatedly(() -> setVoltage(3.0)).named("runSlow (hold)");
}
 
/** Spin the flywheel at 6 volts and hold it there. Never finishes. */
public Command runFast() {
return runRepeatedly(() -> setVoltage(6.0)).named("runFast (hold)");
}
 
/** Stop the flywheel and keep it stopped. Never finishes. */
public Command stop() {
return runRepeatedly(this::stopMotor).named("stop (hold)");
}

Three things happen on the runSlow line.

  • () -> setVoltage(3.0) is a lambda. It does not call setVoltage here. It hands that call to runRepeatedly, which makes it for you every loop.
  • runRepeatedly comes from Mechanism, which is what the class extends.
  • .named(...) gives the command a name. Names show up in logs and on the dashboard, which is what makes one findable when it misbehaves later.

this::stopMotor is the same as () -> stopMotor(), pointing at the private helper from last lesson. Every name ends in (hold) because runRepeatedly has no exit: these run until something else claims the mechanism. Even stop() holds, sending zero every loop rather than once.

Watch out

A hold never finishes

So never make anything wait for one. A hold inside Command.sequence sticks there forever and the sequence never reaches its next step. When a step needs an ending, add it where you use the command rather than writing a new method here: armflywheel.runSlow().until(someCondition). The (hold) in every name is there so a stuck sequence names its own bug in the log.

Check your work

Run WPILib: Build Robot Code. You should see BUILD SUCCESSFUL. If it does not, the error is almost always one of these three.

ErrorCauseFix
NeedsNameBuilderStage cannot be converted to CommandA .named(...) is missing from that method.Name the command. javac prints both types package-qualified.
cannot find symbol: method withPriority(int)A builder method landed after .named(...).Move it in front of the name. Same for whenCanceled.
cannot find symbol: class CommandThe import is missing.Add import org.wpilib.command3.Command;. This stack is never edu.wpi.first.

Check yourself

setVoltage is private on both mechanisms. What does keeping it that way buy you?

In runRepeatedly(() -> setVoltage(3.0)).named("runSlow (hold)"), what does runRepeatedly(...) hand back before .named(...) runs?

You bind driver.a().whileTrue(robot.flywheel.runFast()) and leave the whileFalse off. You release A. What happens?

Pick an answer for each.