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

Coroutines

Command Composition built a routine out of a list of steps. A coroutine is the same routine written as one block of Java, read top to bottom. The block can pause partway through and resume on the same line.

Branchmech-5-CoroutinesAbout 25 minutes
You’ll need
  • An Arm and Flywheel with isAtTarget(), from Finish Conditions.
  • Tuned arm gains from PID Tuning in Tuner X. The branch ships zeros.
  • The simulator running, from Hardware Simulation.

A list of steps runs one at a time, waiting for each to finish. It carries most routines.

Pick the arm and flywheel project back up, then check out mech-5-Coroutines.

Two reasons for a coroutine

Chaining stays the default. The robot template ships its DriveStowDriveauto both ways, and calls the chained version "as far as most routines ever need to go."

When a coroutine earns its keep

A hold has to span several steps. In a list, a hold needs a finish line before the next step can run. A coroutine starts it once and it keeps running underneath.

The logic needs a real loop or a real branch. A list is fixed. A coroutine body is ordinary Java, so while and if work as usual.

Everything else belongs to Command.sequence and Command.race. Do not rewrite a chained routine that works.

Four verbs

A coroutine body takes one argument, an object called coroutine. Four of its methods carry almost every routine.

VerbWhat it does
fork(command)Starts a command and keeps going. It runs underneath until the routine ends.
await(command)Runs a command and stops here until it finishes.
waitUntil(condition)Stops here until the condition comes back true.
yield()Stops here for one scheduler loop, then carries on.

yield is the one you need when the body has a loop of its own. A coroutine can hold a real while (true) loop, with a yield at the bottom of it.

That yield keeps one pass through the loop equal to one robot loop. Leave it out and the loop never hands control back, so nothing else on the robot gets a turn.

THE ONE RULE

Never await a hold

arm.vertical() re-sends its position request every loop and never finishes, so coroutine.await(arm.vertical()) sits on that line for the rest of the match. Nothing errors and nothing logs.

fork is the answer. It starts the hold and returns at once, so the next line runs while the arm keeps holding.

Build the routine

The diff adds one file and changes nothing else: src/main/java/first/robot/opmode/RaiseAndShootOpMode.java. Six steps.

Step 1: The empty shell

RaiseAndShootOpMode.java: the shell
package first.robot.opmode;
 
import first.robot.Robot;
import first.robot.mechanisms.Arm;
import first.robot.mechanisms.Flywheel;
import org.wpilib.command3.Command;
import org.wpilib.command3.Scheduler;
import org.wpilib.opmode.Autonomous;
import org.wpilib.opmode.PeriodicOpMode;
 
@Autonomous(name = "Raise And Shoot")
public class RaiseAndShootOpMode extends PeriodicOpMode {
private final Command routine;
 
public RaiseAndShootOpMode(Robot robot) {
final Arm arm = robot.arm;
final Flywheel flywheel = robot.flywheel;
 
routine =
Command.noRequirements(
coroutine -> {
// Steps 2 to 6 go in here.
})
.named("Raise And Shoot");
}
 
@Override
public void start() {
Scheduler.getDefault().schedule(routine);
}
 
@Override
public void end() {
Scheduler.getDefault().cancel(routine);
}
}

It is a whole OpMode, shaped like TeleopOpMode. Command.noRequirements claims no mechanism of its own, because the forked commands claim theirs.

Build now and Raise And Shoot appears in the autonomous list, doing nothing.

Step 2: Fork the arm hold

First line of the body
// fork, not await: vertical() is a hold and never finishes.
coroutine.fork(arm.vertical());

Run it and the arm barely twitches. That is correct. The body has no lines after the fork, so the routine ends on its first pass, and ending a routine cancels everything it forked.

Step 3: Wait for the arm

Add import static org.wpilib.units.Units.Seconds; first. Every compile runs spotlessApply, which strips an import no line uses yet, so add it again if it vanishes.

Add below the fork
// Always time out a wait in an auto, or a stuck arm freezes the whole match.
coroutine.await(
Command.waitUntil(arm::isAtTarget)
.named("wait for the arm")
.withTimeout(Seconds.of(3.0))); // TODO: time your own arm

Command.waitUntil(arm::isAtTarget) does nothing except finish once the arm arrives, so await is safe on it. The timeout stops a jammed arm from eating the whole autonomous period. Three seconds is a placeholder.

Step 4: The flywheel, same pair

Add below the arm wait
// The arm hold is still running here - that is the point of fork.
coroutine.fork(flywheel.runFast());
coroutine.await(
Command.waitUntil(flywheel::isAtTarget)
.named("wait for the flywheel")
.withTimeout(Seconds.of(3.0)));

Two forks are live now. The arm still holds 90° while the flywheel climbs to 75 rotations per second. A list of steps would need a Command.race around every later step.

Step 5: Shoot

The last line in the body
coroutine.wait(Seconds.of(1.0)); // shoot

Nothing there fires a shot: this branch has an arm, a flywheel, and no feeder, so the wait stands in for one. A wait pauses for a fixed time, where waitUntil pauses for a condition, and both forks keep running through it.

Step 6: Fall off the end

There is no cleanup step. The body runs out of lines, the routine finishes, and both forks are canceled.

Canceled is not stopped. idle() sends no output and never clears the last request, so the flywheel keeps spinning. End a mid-match routine with explicit stop steps.

The finished file

Compare it against what you typed, indentation included.

Loading file...

Check your work

Build it, run it, then break it on purpose.

  1. Build. If it compiles, .named(...) and .withTimeout(...) are in the right order.
  2. Start the simulator and pick Raise And Shoot, or run ./gradlew simulateJavaAgent -Pmode=auto:"Raise And Shoot". That name is the @Autonomous string, not the class name.
  3. Time the run. A healthy one is the arm, plus the flywheel, plus one second.
  4. Now break it. Change the first line to coroutine.await(arm.vertical()) and run again. The arm moves and nothing else ever happens. Put the fork back.
Check

You should see

  • The arm swinging to vertical, 0.25 rotations, and staying there while the flywheel reaches 75 rotations per second.
  • The routine ending a second later, both holds released.
What you seeCause
Seven seconds every run, nothing arrivesBoth waits timed out: 3 + 3 + 1. The branch ships the arm gains at 0.0. Tune it first.
One thing happens, then nothingA hold inside await. Only self-finishing commands belong there.
Will not compile, on a waitUntil line.withTimeout(...) written before .named(...).
cannot find symbol: SecondsThe import is missing, or spotless stripped it before a line used it.

State Machines is next, and it goes back to chaining. Drive to Tag returns here with a body that is one while (true) loop.

The template's coroutine OpMode

Check yourself

01

Why does the routine call coroutine.fork(arm.vertical()) instead of coroutine.await(arm.vertical())?

02

Why is it .named("wait for the arm").withTimeout(Seconds.of(3.0)) and not the other way around?

03

What does the .withTimeout(Seconds.of(3.0)) on each wait protect you from?

04

The coroutine body forks the arm hold and the flywheel hold, waits a second, and then runs out of lines. What happens to the two forked holds?

05

Your routine drives to a pose, then drives to a second pose, and nothing needs to be held across both legs. Which style should you use?

Pick an answer for each.