Coroutines
Finish Conditions ran this routine off a held button, with nothing bounding its waits. Autonomous has no button and nobody to let go of one, so every wait here gets a time limit and a way out.
- An
ArmandFlywheelwithisAtTarget(), and the Y-button coroutine, from Finish Conditions. - Tuned arm gains from PID Tuning in Tuner X. The branch ships zeros.
- The simulator running, from Hardware Simulation.
Pick the arm and flywheel project back up, then check out mech-5-Coroutines.
Two reasons for a coroutine
A sequence is already a small coroutine that awaits each member in turn. It stays the default, and a chained routine that works should stay chained.
Write the body yourself for one of two reasons. A hold has to span several steps, which a list cannot do. Or the logic needs a real loop or branch, and a coroutine body is ordinary Java, so while and if work as usual.
The core five
A coroutine body takes one argument, an object called coroutine. These five calls on it cover nearly every routine, in the order you will reach for them.
// 1. Wait for a condition, with a limit. On a timeout, stop what you started and leave.if (coroutine.waitUntil(() -> robot.arm.isAtTarget(), Seconds.of(3.0)).timedOut()) { return;} // 2. Start a hold and keep going. It runs underneath until the routine ends.coroutine.fork(robot.arm.vertical()); // 3. Run a step that ends by itself, and wait here until it does.coroutine.await(raiseArm); // the .until(...) step from Finish Conditions // 4. Wait a fixed time. Forked holds keep running through it.coroutine.wait(Seconds.of(1.0)); // 5. Inside a loop of your own: give up the rest of this robot loop.while (!robot.arm.isAtTarget()) { coroutine.yield();}waitUntil returns a WaitResult. House style calls .timedOut() on it in the same line, and stores it only when it is read twice. It is declared inside Coroutine, so the bare name needs import org.wpilib.command3.Coroutine.WaitResult;.
fork and await return a ForkResult for a command that could not start. The routine cancels itself when that happens, so you can ignore it. park, awaitAll and awaitAny exist too. Nothing here needs them.
Example 5 is what waitUntil does inside. The yield makes one pass one robot loop, and without it nothing else on the robot gets a turn. If while is new, do the Loops module of Codecademy's Learn Java before Drive to Tag.
Six states, one routine. Play it, or step through one state at a time.
coroutine.fork(robot.arm.vertical());runs · returns on the same loopif (coroutine.waitUntil( () -> robot.arm.isAtTarget(), Seconds.of(3.0)).timedOut()) { return;}waiting · 0.50 scoroutine.fork(robot.flywheel.runFast());runs · returns on the same loopif (coroutine.waitUntil( () -> robot.flywheel.isAtTarget(), Seconds.of(3.0)).timedOut()) { coroutine.fork(robot.flywheel.stop()); return;}waiting · 0.60 scoroutine.wait(Seconds.of(1.0));waiting · 1.00 scoroutine.fork(robot.flywheel.stop());// then the body runs out of linesstop sent · routine finishesrobot.arm.vertical()runRepeatedly · never finishesrobot.flywheel.runFast()runRepeatedly · never finishescoroutine.await(robot.arm.vertical());waiting · for the rest of the matchrobot.arm.vertical()holding 90° correctly, and never finishingvertical() is built with runRepeatedly, so it never finishes on its own. await keeps the body on that line until the command it was handed completes, and that one never will. The arm holds 90° correctly and the rest of the routine is never reached. No error, no log line, nothing on the dashboard except a routine that sits there.
await is safe on a command that finishes by itself. The waits in the diagram above are coroutine.waitUntil, which watches a condition, and the holds are forked.
House rules
House rules
- A button binds a hold,
runRepeatedly(...).named("x (hold)"), withwhileTrue. A flywheel adds.whileFalse(robot.flywheel.stop()). - A routine across mechanisms is
Command.noRequirements(coroutine -> body(coroutine))with.named(...). The body is a private method, so areturnplainly leaves the routine. - A body that drives one mechanism is that mechanism's
run(coroutine -> ...), written inside its class next torunRepeatedly. - Fork holds, await steps. A hold never finishes, so
awaiton one never returns. The Y button does that on its last line on purpose, to run until release. - Waits are
coroutine.waitUntil(...). Never build a wait out of aCommandinside a body. - Every wait in an autonomous has a timeout, about twice the time you measured.
- Every exit stops what it started. Canceling is not stopping, so fork the flywheel's
stop()before eachreturn. The arm needs none, because its last position request holds it.
Rules two and three differ in what they claim. run(...) holds its mechanism for the whole body. noRequirements claims nothing, and each fork claims its own mechanism only while it runs.
Build the routine
The diff adds one file and changes nothing else: src/main/java/first/robot/opmode/RaiseAndShootOpMode.java. Four steps.
Step 1: The empty shell
package first.robot.opmode; import first.robot.Robot;import org.wpilib.command3.Command;import org.wpilib.command3.Coroutine;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 Robot robot; private final Command routine; public RaiseAndShootOpMode(Robot robot) { this.robot = robot; routine = Command.noRequirements(coroutine -> raiseAndShoot(coroutine)).named("Raise And Shoot"); } private void raiseAndShoot(Coroutine coroutine) { // Steps 2 to 4 go in here. } /** No trigger owns this routine, so the OpMode starts and stops it. */ @Override public void start() { Scheduler.getDefault().schedule(routine); } @Override public void end() { Scheduler.getDefault().cancel(routine); }}Paste the whole shell. No button binds this routine, so start() and end() schedule and cancel it. Build now and Raise And Shoot appears in the autonomous list, doing nothing.
Step 2: Fork the arm hold
// fork, not await: vertical() is a hold and never finishes.coroutine.fork(robot.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, with a limit
Add import static org.wpilib.units.Units.Seconds; first. Every build runs Spotless, which strips an import no line uses yet, so add it again if it vanishes.
// TODO: time your own arm.if (coroutine.waitUntil(() -> robot.arm.isAtTarget(), Seconds.of(3.0)).timedOut()) { // The flywheel never started, so there is nothing to stop. return;}This is the Y button's wait with a limit added. Three seconds is a placeholder: use the time you wrote down at the end of Finish Conditions, doubled.
Step 4: The flywheel, then the shot
// The arm hold is still running. That is the point of fork.coroutine.fork(robot.flywheel.runFast()); if (coroutine.waitUntil(() -> robot.flywheel.isAtTarget(), Seconds.of(3.0)).timedOut()) { coroutine.fork(robot.flywheel.stop()); return;} coroutine.wait(Seconds.of(1.0)); // shoot // stop() replaces runFast() and sends its zero on this loop, before the routine ends.coroutine.fork(robot.flywheel.stop());Two forks are live now, the arm holding 90° while the flywheel climbs to 75 rotations per second. This branch has no feeder, so the one-second wait stands in for the shot.
Every way out from here forks stop() first. It shares the flywheel with runFast(), so it replaces it, and a forked command runs its first pass on the spot. The zero goes out before the routine ends.
Check your work
Build it, run it, then break it on purpose.
- Start the program with WPILib: Hardware Sim Robot Code and pick Raise And Shoot in the Driver Station. That name is the
@Autonomousstring, not the class name. - Time the run. A healthy one is the arm, plus the flywheel, plus one second, and then the flywheel coasts down.
- Change the arm's tolerance to
Degrees.of(0.001)and run again. Then delete theifand itsreturnaround the arm's wait and run once more. Put both back. - Last one. Change the first line to
coroutine.await(robot.arm.vertical())and run again. The arm moves and nothing else ever happens. Put theforkback.
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. The flywheel coasts to a stop and the arm keeps holding vertical.
- With the tolerance broken, the routine giving up after three seconds and the flywheel never starting. Without the
if, it shoots anyway, at an angle nobody checked.
| What you see | Cause |
|---|---|
| Three seconds, then the routine ends. The arm never gets there | The first wait timed out and returned. The branch ships the arm gains at 0.0. Tune it first. |
| One thing happens, then nothing | A hold inside await. Only self-finishing commands belong there. |
| The full seven seconds, and it shoots anyway | A wait with no timedOut() check around it. The routine cannot tell a timeout from an arrival. |
| The routine ends and the flywheel keeps spinning | A return, or the last line, with no stop() forked before it. |
cannot find symbol: Seconds | The import is missing, or Spotless stripped it before a line used it. |
Logging is next. It is how you read back a routine that ran with nobody watching.
Check yourself
Why does the routine call coroutine.fork(robot.arm.vertical()) instead of coroutine.await(robot.arm.vertical())?
The Y button in Finish Conditions wrote coroutine.waitUntil(() -> robot.arm.isAtTarget()) with no timeout. Why does the same wait need one here?
You write coroutine.waitUntil(() -> robot.arm.isAtTarget(), Seconds.of(3.0)); and ignore what it returns. The arm jams. What does the routine do?
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?
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?
You delete the last line, coroutine.fork(robot.flywheel.stop()), and run the routine. What does the flywheel do after the routine ends?