Gray Matter
WorkshopOpModes
WPILib 2027 is still in alpha: these pages change as the APIs settle.
LESSON 13

OpModes

Every way the robot can run is its own class with an annotation on top. The driver station lists those classes by name, and picking one builds it. There is no RobotContainer in this project.

8 minutes
You’ll need
  • The project from Project Setup, building clean.
  • Commands and button bindings from Writing Commands.
  • The scheduler vocabulary from The Command Framework.

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.

An OpMode is one way the robot can run: driver control, a single autonomous routine, a pit procedure that zeroes an arm before a match.

Nothing registers these classes anywhere. The framework finds them by their annotation, and the driver station shows what it found.

Three OpMode roles

All three are ordinary Java classes. The annotation decides which name the driver station shows, and it says what the mode is for.

Driver control
@Teleop
Controller bindings and the defaults a driver expects. They exist only while teleop is the selected mode.
Preplanned
@Autonomous
One routine, named on the driver station, scheduled when the mode starts and canceled when it ends.
Pit work
@Utility
Zeroing, characterization, diagnostics. Keeping these out of teleop means a driver cannot trip one in a match.

One routine per class, not one class holding four of them. Four autonomous plans mean four @Autonomous classes and four names on the list.

The Teleop OpMode

The commands you wrote on Writing Commands call nothing yet. This is the class that calls them. Project Setup left a generated MyTeleop.java in opmode/, already carrying @Teleop. That is the file you edit. Replace its body with the whole file from the branch, minus the copyright header.

src/main/java/first/robot/opmode/MyTeleop.java
package first.robot.opmode;
 
import first.robot.Robot;
import org.wpilib.command3.button.CommandNiDsXboxController;
import org.wpilib.opmode.PeriodicOpMode;
import org.wpilib.opmode.Teleop;
 
/**
* The driver's controls. The framework builds this class when "Teleop" is picked on the driver
* station. The button bindings made in the constructor belong to this OpMode, and the framework
* removes them on a mode switch. No cleanup code needed.
*
* <p>The buttons here run the arm and flywheel commands.
*/
@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());
 
// Right trigger: spin fast while held, drop back to the slow voltage when released.
driver.rightTrigger().whileTrue(robot.flywheel.runFast()).whileFalse(robot.flywheel.runSlow());
 
// A: spin fast while held, stop when released.
driver.a().whileTrue(robot.flywheel.runFast()).whileFalse(robot.flywheel.stop());
}
}

Building only the armflywheel? Delete the robot.flywheelarm bindings along with them. Same reason as the field: they call commands on a class that does not exist in your project.

Every whileTrue has a whileFalse behind it. whileTrue schedules the command on the press and cancels it on the release.

THE TRAP

Canceling a command does not stop the motor

The motor is not running in your code. It is running on the motor controller, which keeps applying the last request it was sent until something sends a different one. So canceling a command does not affect the controller.

So something else has to take over. That is either a whileFalse, as here, or a default command on the mechanism. With neither, releasing the trigger leaves the armflywheel running on the last thing it was told.

Mode boundaries

Most teleop classes need neither method below. Bindings made in the constructor are enough, and the framework removes them when the mode changes. There is no cleanup code to write. Bind in the constructor, but never drive a motor from it: the robot can still be disabled when that code runs.

The lifecycle shape
@Override
public void start() {
// Called once when this OpMode becomes active.
}
 
@Override
public void end() {
// Called once when this OpMode stops being active.
}

Autonomous is where the two earn their place. start() schedules the routine and end() cancels it. Pair them every time. Hit disable partway through a run and end() fires, so the routine stops on that loop rather than running on into the next mode.

Utility modes use the same boundary. Begin the calibration in start(), stop it in end(), and the mode is safe to leave at any point.

Where behavior lives

One question settles most of it. What is the smallest scope where this behavior still works? Put it there.

BehaviorHomeAnywhere else
Driver buttonsThe @Teleop classA binding in Robot stays live in every mode.
One autonomous routineIts own @Autonomous classA routine with no annotation has no name to select.
Zeroing, characterizationA @Utility classOn a driver button, someone starts it during a match.
A binding every mode needsThe Robot constructorCopied into each OpMode, the copies drift apart.
Motor IDs and gainsThe mechanismIn an OpMode, two modes can configure the same motor.

Check your work

Run WPILib: Build Robot Code. You should see BUILD SUCCESSFUL. Nothing runs until Hardware Simulation, so the rest of this is a read through your own class.

Check yourself

You bind whileTrue(robot.arm.runFast()) and leave the whileFalse off. You release the trigger. What does the arm do?

You switch from Teleop to Autonomous. What happens to the bindings made in the teleop constructor?

A binding has to brake the drivetrain whenever the robot is disabled, in every mode. Where does it go?

Your team has four autonomous routines. How many classes, and what picks between them?

Pick an answer for each.