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

Robot.java

One Robot object is built at startup and stays alive while modes come and go. It holds the mechanisms, runs the command scheduler once per loop, and starts anything the whole match needs. Mode-specific code stays out of it.

11 minutes
You’ll need
  • The project from Project Setup, building clean.
  • The scheduler vocabulary from The Command Framework.
  • The mode boundary from OpModes.

Pick teleop and the framework builds a teleop object. Leave teleop and that object is thrown away. Pick autonomous and the same thing happens again, with a different class. The arm those modes drive is one object, built once. It is the same arm in the last second of the match.

Robot.java is where that one object is created. Everything in the file outlives every mode, and nothing in the file belongs to one mode. Those two rules decide everything that goes in it.

The whole file

Robot.java: the complete shape
package first.robot;
 
import first.robot.mechanisms.Arm;
import first.robot.mechanisms.Flywheel;
import org.wpilib.command3.Scheduler;
import org.wpilib.framework.OpModeRobot;
 
public class Robot extends OpModeRobot {
// One shared object for each physical mechanism.
public final Arm arm = new Arm();
public final Flywheel flywheel = new Flywheel();
 
public Robot() {
// Start course-wide services and add truly global bindings here.
}
 
@Override
public void robotPeriodic() {
Scheduler.getDefault().run();
}
}

Two fields and one method. The fields are public so an OpMode handed a Robot can reach robot.arm directly. They are final so that one arm object serves the whole match.

A second new Arm() elsewhere in the project does not fail. It compiles and it runs. Now two objects configure the same motor, and the scheduler counts them as unrelated mechanisms, so two commands can drive one gearbox at once. Nothing warns you at build time. You find out when the arm fights itself on the bench.

Field initializers run before the constructor body, so both mechanisms already exist by the first line of Robot(). robotPeriodic() begins after that constructor returns, and it keeps being called in every mode, including while the robot is disabled.

One scheduler call per loop

The framework calls robotPeriodic() every 20 milliseconds, 50 times a second, for as long as the robot has power. The single line inside it gives the scheduler one pass. It checks the triggers, queues a default command for any mechanism with nothing to do, and gives every running command one step.

Delete that line and the build still passes. The mechanisms are still built. Nothing in the project ever moves again.

The callWhat happensWhat you see
MissingNo trigger is ever checkedA clean build, a mode list, and a robot that ignores every button.
Inside an OpModeThe scheduler stops while that mode is not selectedButtons work in teleop, and an autonomous routine sits on its first step.
Called twiceEvery command takes two steps a tickA sequence gets through its steps in half the usual loops, and nothing reports an error.

The middle row is the common one, and it hides well. A team debugging teleop adds a scheduler call to the teleop class, teleop starts working, and autonomous quietly stops advancing until the next match.

CRITICAL

One call, one place

Scheduler.getDefault().run() belongs in Robot.robotPeriodic() and nowhere else. Leave it there even when a mode looks like it needs a copy of its own. If commands are not advancing, the cause is somewhere else.

Course-wide setup

The constructor runs once, at startup, after the mechanism fields exist and before any mode is selected. It is the only place in the project that can set something up for every mode at once. Four kinds of thing earn a spot in it.

  • Logging. DataLogManager.start() and the DriverStation log are two lines, and Logging covers what they record.
  • A service that two mechanisms share. Build it here, then hand the same object to both of them.
  • A safety binding that has to hold in every mode, not only in the one mode where a driver would notice it missing.
  • Per-loop work that is not a command, registered with Scheduler.getDefault().addPeriodic(...). Registering it costs one line, and the work itself happens on later loops.

Keep the constructor short. Startup waits on it, and no command runs and no motor moves until it returns. Reading a file or waiting on a camera here delays the whole robot.

Other things get put in here by mistake, and each one already has a home. Driver buttons go in the teleop class. An autonomous routine goes in its own @Autonomous class. Motor IDs, inversions, and gains go in the mechanism. A one-off calibration control goes in a @Utility class.

The reason is the same every time. A binding made in this constructor is live in every mode. Put a calibration routine on a driver button and someone can start it mid-match.

The ownership test

Where a new field goes comes down to lifetime. Ask how long the object has to stay alive, and the answer names the file.

Needs to live forHomeIn the wrong home
The whole match, and it owns hardwareA public final field on RobotBuilt in an OpMode, the motor is reconfigured on every mode change.
One operating modeA field on that OpModeLeft on Robot, its bindings keep firing in autonomous.
One run of one commandA local inside the command bodyHeld in a field, the second run starts on the first run's leftovers.
The whole session, no hardwareThe Robot constructorStarted in an OpMode, it stops the moment the mode changes.

The controller is the field people get wrong. Both the controller object and its bindings belong to the teleop class. Bound in the Robot constructor instead, the same button still fires while an autonomous routine is running.

Check your work

There is no button to press for this lesson, so prove the lifetime instead. Two print lines and one mode switch show it.

  1. Run ./gradlew build and wait for BUILD SUCCESSFUL.
  2. Search the whole project for getDefault().run. Exactly one hit, inside robotPeriodic(). Search for new Arm( and get one hit as well.
  3. Add System.out.println("Robot built"); as the first line of the Robot constructor, and System.out.println("Teleop built"); as the first line of your teleop constructor.
  4. Start the simulator with ./gradlew simulateJava. Watch the console while you pick teleop, switch to another mode, and pick teleop again.
  5. Delete both print lines once the counts match.
Check

You should see

  • Robot built printed once, at startup, and never again.
  • Teleop built printed every single time teleop is selected.
  • One scheduler call in the project, and it is in Robot.
  • One object per mechanism, all of them built in Robot.java.

A second Robot built line means something other than the framework is constructing a Robot. Two hits on new Arm( mean the arm is being built somewhere it should be borrowed instead. Fix that before Mechanisms, where every command you write reaches the hardware through these fields.