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

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.

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

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. Selecting a mode constructs it, along with every binding in its constructor. Selecting a different mode takes those bindings away.

Three OpMode roles

All three are ordinary Java classes. The annotation decides which name the driver station shows, and it tells the next person opening the file 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 smallest Teleop OpMode

Project Setup left two generated files in opmode/. MyTeleop is the one this section rewrites. Rename it to TeleopOpMode before you start. Press F2 on the class name and VS Code renames the file with it. MyAuto becomes LeaveStartAuto in Autonomous, so leave it alone for now.

TeleopOpMode.java: one button, one mode
package first.robot.opmode;
 
import first.robot.Robot;
import org.wpilib.command3.button.CommandNiDsXboxController;
import org.wpilib.opmode.PeriodicOpMode;
import org.wpilib.opmode.Teleop;
 
@Teleop(name = "Driver Control")
public class TeleopOpMode extends PeriodicOpMode {
private final CommandNiDsXboxController driver = new CommandNiDsXboxController(0);
 
public TeleopOpMode(Robot robot) {
driver.a().onTrue(robot.arm.runSlow()).onFalse(robot.arm.stop());
}
}

The constructor is handed the one Robot, and that is the only way in to the arm. An OpMode never builds a mechanism of its own: two modes would end up configuring the same motor. The bindings are made once, here, not on every loop.

There is no loop in here. The scheduler checks the trigger on every robot tick and schedules the command when it fires.

Inside the constructor

The robot program constructs the OpMode the moment someone picks it on the driver station. That can happen while the robot is still disabled, and constructor code runs anyway. Three things belong in there, and three do not.

  • Trigger bindings for this mode, which is most of what a teleop class holds.
  • A command built and kept in a field, ready for start() to schedule.
  • A default command, set with robot.arm.setDefaultCommand(...). It is a binding like any other, so it lasts as long as this mode does.
  • No motor output. The robot may still be disabled when this code runs.
  • No state you need after a mode switch. The OpMode is rebuilt each time; Robot is not.
  • No motor configuration. IDs, inversions, and gains belong to the mechanism.

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.

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.
COMMON MIX-UP

An OpMode is not a mechanism

The OpMode decides when an action is available. The mechanism owns the motor and hands out the command. If changing a driver button means editing motor configuration, the two have been mixed together.

Check your work

Build it, then go look at the list of modes. Deploy and Run covers the simulator properly later; this is the short version.

  1. Run ./gradlew build. Nothing else is worth checking until that finishes clean.
  2. Read your teleop class once. Public class, annotation with a name, public constructor taking Robot, every binding inside it.
  3. Start the simulator with ./gradlew simulateJava, then read the mode list on the driver station.
  4. Pick your teleop mode, enable, and press the bound button. Then switch modes and press it again.
Check

You should see

  • Every mode class you wrote, listed by its annotation name.
  • The button running its command while teleop is selected.
  • The same button doing nothing after the mode changes.

A mode missing from that list is one of four things:

  • The class is not public, or it is abstract.
  • The annotation carries no name.
  • The class sits outside first.robot and its subpackages.
  • The constructor does not take a Robot.

Fix the class, rebuild, and the name appears.