Gray Matter
WorkshopThe Command Framework
WPILib 2027 is still in alpha: these pages change as the APIs settle.
LESSON 09

The Command Framework

Robot code here is made of triggers, mechanisms and commands. Underneath them a scheduler runs fifty times a second and settles which command owns which motor.

10 minutes
You’ll need
  • The vocabulary from Java Basics: class, field, method, constructor, lambda.
When
Triggers
A controller button, a sensor reading, a comparison you wrote yourself. Attach a command to a trigger and the scheduler watches it for you.
What
Mechanisms
The arm. The flywheel. The drivetrain. Each one is a class that implements Mechanism, with its motors and sensors as private fields and its configuration done once, in the constructor.
How
Commands
Named actions on a mechanism. Almost every command in Workshop 3 is a hold: it re-sends the same request and never ends by itself.

The scheduler loop

None of the three does anything on its own. robotPeriodic() is called for you for as long as the robot has power. The gap between calls is 20 milliseconds, so it runs 50 times a second.

Robot.java: the one call that runs the scheduler
public class Robot extends OpModeRobot {
@Override
public void robotPeriodic() {
Scheduler.getDefault().run();
}
}

One call does the whole job. It checks every trigger, starts and cancels commands from what it finds, and runs any background code you register.

One command per mechanism

A command declares which mechanisms it needs. The command from arm.runFast() needs the arm, and while it runs, it owns the arm. No other command touches that motor at the same time.

Every command has a priority. A second command takes a mechanism only if its priority is the same or higher than the command already holding it. Every command in this workshop uses the same priority, so a new one always gets to run.

Canceling is not stopping

A mechanism can be given a default command, which the scheduler runs whenever nothing else has claimed it. Set one with setDefaultCommand(...) and give it a low priority, so anything can take the mechanism from it. Set none and an unclaimed mechanism has nothing running on it, which sends nothing at all to the motor.

Nothing commanding the motor is not the same as the motor being off. Phoenix keeps applying the last request it was given, so canceling a command does not stop hardware. Hardware Simulation shows it on the bench.

Where bindings live

A mechanism owns hardware. An OpMode decides what the robot does during one part of the match. That means which buttons do what while a driver is in control, or which routine runs in autonomous.

The mechanisms sit on the same Robot.java as that scheduler call.

Robot.java: the same file, with the mechanisms added
public class Robot extends OpModeRobot {
// The robot's mechanisms. Public so OpModes can use them.
public final Arm arm = new Arm();
public final Flywheel flywheel = new Flywheel();
 
@Override
public void robotPeriodic() {
Scheduler.getDefault().run();
}
}

Each mode is a separate class marked @Teleop, @Autonomous or @Utility. That marking is how it shows up in the list on the driver station. Every mode is handed the Robot, so it can reach the arm and the flywheel.

MyTeleop.java: the shape of a mode class
@Teleop(name = "Teleop")
public class MyTeleop extends PeriodicOpMode {
private final CommandNiDsXboxController driver = new CommandNiDsXboxController(0);
 
public MyTeleop(Robot robot) {
// Left trigger: push the arm up while held, stop when released.
driver.leftTrigger().whileTrue(robot.arm.runFast()).whileFalse(robot.arm.stop());
}
}

The bindings live in the constructor. The framework builds this class when someone picks Teleop, and throws it away on a mode switch. The bindings go with it, so no binding from auto ever fires during teleop.

Commands with no finish condition

Almost every command in this workshop is built with runRepeatedly(...). The body runs every loop. If you have seen a while loop before, this is one. Each pass runs the body, then waits for the next loop, 20 milliseconds later.

Nothing inside it decides when to stop, so ending it is somebody else's job. Releasing the trigger is what does it above: whileTrue cancels the command on the way down, and whileFalse schedules arm.stop() in its place.

The reason to write commands this way is visibility. A command that keeps running stays the command on its mechanism, so a log always names what has the arm right now.

Here is a real one, from the arm you build in Writing Commands.

Arm.java: one command, from the arm you build later
/** Push the arm at 6 volts and keep pushing. Never finishes. */
public Command runFast() {
return runRepeatedly(() -> setVoltage(6.0)).named("runFast (hold)");
}

runRepeatedly re-runs setVoltage every loop, so the six-volt request never goes stale. Every command built that way ends with the (hold) suffix, which is a promise from whoever wrote it: this command has no ending.

Six volts is a push, not a position. The arm ends up wherever gravity and friction let it.

Check your work

No editor yet. Take the left-trigger binding in MyTeleop above. Write down which command owns the arm before the press, while the trigger is held, and after the release.

Check

You should have

  • Before the press: nothing. The arm is unclaimed.
  • While held: runFast (hold), started by whileTrue.
  • After the release: stop (hold), started by whileFalse as runFast is canceled.

Check yourself

Robot.java calls Scheduler.getDefault().run() inside robotPeriodic(). What breaks if you delete that line?

One command has the arm. A button fires and a second command that also needs the arm gets scheduled. Both have the ordinary priority. Who ends up with the arm?

A routine has been stuck for eight seconds, and the command it is sitting on is named "runFast (hold)". What does that name tell you?

No command is claiming the flywheel. What is the mechanism doing?

Pick an answer for each.