Gray Matter
WorkshopFinish Conditions
WPILib 2027 is still in alpha: these pages change as the APIs settle.
LESSON 22

Finish Conditions

Command Composition ended every step with a stopwatch, and that number was a guess. This lesson ends a step when a sensor reports the mechanism arrived. The stopwatch stays on as a backstop.

About 20 minutes
You’ll need
  • Command.sequence and .withTimeout(...), from Command Composition.
  • An arm position hold that keeps asking for one angle, and gains that reach it.
  • Lambdas and method references, from Java Basics.
  • The simulator running, from Hardware Simulation.

A timeout ends a step after a fixed number of seconds and never asks whether anything happened. Two seconds is plenty for the arm on a fresh battery and short on a tired one.

The arm already carries a sensor that says where it is. Compare that reading against the angle the step asked for, and the step can end on arrival rather than on the clock.

Timeouts and conditions

.until(...) wraps a command and ends it on the first loop a condition comes back true. That condition is a BooleanSupplier: any small piece of code that answers true or false when it is asked. The scheduler asks about fifty times a second.

Hand it a method reference. Written as arm::isAtTarget, the condition passes the method itself, so it can be called again on every loop. Add the parentheses and arm.isAtTarget() runs the method on the spot, passing one frozen answer. That will not compile: boolean cannot be converted to BooleanSupplier.

.until(...) hands back a builder rather than a Command, the same way Command.sequence(...) did. .named("...") closes it. Leave the name off and the build fails, because a builder is not a Command.

The arrival question

The mechanism owns the comparison. Its units, its target, and its tolerance are already in that one file. Put the arithmetic there too, and every call site gets one readable question instead.

Arm.java: the arrival check
private static final double POSITION_TOLERANCE_ROT = 0.01;
 
public boolean isAtTarget() {
double error = targetPositionRot - getPositionRot();
return Math.abs(error) <= POSITION_TOLERANCE_ROT;
}

targetPositionRot is the angle the last position request asked for. The other half of the subtraction, getPositionRot(), reads the CANcoder through the motor. The units are mechanism rotations, because Mechanisms made that encoder the feedback source. The tolerance is the number you pick, and a hundredth of a rotation is about three and a half degrees.

NO EXACT EQUALITY

Sensors jitter

Never wait for position == target. Ask for 0.25 rotations and you read 0.2497, then 0.2503. An exact comparison is false forever, so a step waiting on one never ends. Give the tolerance the same units as the target.

Both endings on one step

arm.vertical() is a hold. It re-sends its position request every loop and never finishes, so it suits a held button and is useless as a member of a list. One call site turns it into a step.

A condition to finish on, a timeout to give up on
import static org.wpilib.units.Units.Seconds;
 
Command raiseArm =
arm.vertical()
.until(arm::isAtTarget)
.named("vertical until at target")
.withTimeout(Seconds.of(2.0));

vertical() itself is untouched and still reusable anywhere. The condition is the ending you want. The timeout is the ending you get when a sensor dies or the arm jams. It goes after .named(...), since .withTimeout(...) is a method on Command and not on the builder.

What a timeout proves

That the waiting is over. Nothing else. If the next step assumes the arm arrived, ask arm.isAtTarget() again before running it. Or log the answer, so a post-match file separates a success from a step that hit its timeout.

A routine is those steps in order. Every member needs an ending, including the last one.

Every member ends, so the group ends
Command score =
Command.sequence(
arm.vertical()
.until(arm::isAtTarget)
.named("raise arm")
.withTimeout(Seconds.of(2.0)),
flywheel.runFast().withTimeout(Seconds.of(1.0)),
flywheel.stop().withTimeout(Seconds.of(0.1)))
.named("Score");

The two flywheel members have no arrival to wait for, so their timeouts are the intended ending rather than a backstop. Spinning for one second is the instruction. flywheel.stop() is a hold too, so without a timeout on it the group never finishes either. A tenth of a second is long enough to send zero and release the mechanism. Autonomous stops its drivetrain the same way.

Conditions that never come true

A condition that cannot go true is as bad as a bare hold. The sequence sits on that step, nothing throws, nothing logs, and the arm keeps pushing. A fifteen-second autonomous period spends all fifteen on step one.

Never true
Tolerance too tight
The arm settles half a degree outside the band and stops there. The plot looks fine. The routine does not move.
Never changes
A dead sensor
A CANcoder off the bus reports one value forever, so isAtTarget() gives the same answer every loop whatever the arm does.
True too early
Passing through
A fast mechanism crosses the target for one loop on its way past. The step ends while it is still moving.

The timeout covers the first two. It cannot help with the third. For a mechanism that overshoots, require the reading to stay inside tolerance for several loops in a row. Keep that behind the same isAtTarget(), so no call site changes.

Check your work

Run it in the simulator, then break it on purpose. You are done when you can tell the two endings apart without watching a clock.

  1. Add isAtTarget() to Arm, then bind raiseArm to a button with onTrue in your TeleopOpMode. It is a step now, so it ends itself.
  2. Press it once and time how long the step takes to end.
  3. Set POSITION_TOLERANCE_ROT to 0.0001 and press it again.
  4. Leave the tolerance broken, drop .withTimeout(...), and press it once more.
Check

You should see

  • The arm reaching its angle and the step ending well under two seconds.
  • At 0.0001, the step running the full two seconds every time.
  • With the timeout gone as well, the arm pushing until you disable.

Write down the tolerance you settled on and how long the step took at it. That time is the floor for any timeout on this arm. Double it and you have a backstop that will not fire on a good run.

Check yourself

01

Why does isAtTarget() compare against a tolerance instead of checking whether the position equals the target?

02

What does arm::isAtTarget hand to .until(...), and why does arm.isAtTarget() not work in the same place?

03

arm.vertical().until(arm::isAtTarget) on its own will not compile. What is missing?

04

A step ends after exactly the 2.0 seconds its .withTimeout(...) allowed. What do you know about the arm?

05

The arm settles a fraction outside tolerance, so the routine sits on that step for the rest of the match. What keeps one bad step from costing the whole autonomous period?

06

Why is the last member of the sequence flywheel.stop().withTimeout(Seconds.of(0.1)) rather than flywheel.stop()?

Pick an answer for each.