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.
Command.sequenceand.withTimeout(...), from Command Composition.- An arm position hold, with gains that reach the angle it asks for.
- Lambdas, from Java Basics.
- The simulator running, from Hardware Simulation.
What mechanism are you working on?
The lesson below is written for the one you pick. Switch back any time to read it for the other.
Timeouts and conditions
.until(...) wraps a command and ends it on the first loop a condition comes back true. That condition is a BooleanSupplier: a small piece of code that answers true or false when it is asked. The scheduler asks about fifty times a second.
Hand it a lambda. () -> robot.arm.isAtTarget() is a question the scheduler can ask on every loop. Drop the () -> and the method runs on the spot, passing one frozen answer. The build stops on boolean cannot be converted to BooleanSupplier.
.until(...) returns a builder, not a Command, the same way Command.sequence(...) did. .named("...") closes it, and leaving it off fails the build.
The arrival question
The mechanism owns the comparison. Its units, its target and its tolerance already live in that file. Put the arithmetic there too, and every call site gets one readable question.
private final Angle tolerance = Degrees.of(1.0); /** Where the arm is now, straight off the CANcoder. */public Angle getPosition() { return encoder.getPosition().getValue();} /** Where the last position request asked it to go. */public Angle getTargetPosition() { return positionOut.getPositionMeasure();} /** True when the arm has reached its target angle. */public boolean isAtTarget() { return getPosition().isNear(getTargetPosition(), tolerance);}private final AngularVelocity tolerance = RotationsPerSecond.of(0.5); /** How fast the wheel is turning now, straight off the motor. */public AngularVelocity getVelocity() { return motor.getVelocity().getValue();} /** The speed the last velocity request asked for. */public AngularVelocity getTargetVelocity() { return velocityOut.getVelocityMeasure();} /** True when the flywheel has reached its target speed. */public boolean isAtTarget() { return getVelocity().isNear(getTargetVelocity(), tolerance);}getPosition() reads the CANcoder. getTargetPosition() asks the request object where it was last told to go. Both return an Angle, so nothing here can mix up rotations and degrees. isNear is true when the two are within tolerance, and one degree is the arm's.
getVelocity() reads the motor. getTargetVelocity() asks the request object what speed it was last told to hold. Both return an AngularVelocity, so nothing here can mix up rotations a second and RPM. isNear is true when the two are within tolerance, and half a rotation a second is the flywheel's.
Sensors jitter
Never wait for positionvelocity == target. Ask for 0.25 rotations and you read 0.2497, then 0.2503.Ask for 75 rotations a second and you read 74.98, then 75.03. An exact comparison is false forever, so a step waiting on one never ends.
Both endings on one step
robot.arm.vertical() is a hold. It re-sends its position request every loop and never finishes, which suits a held button and is useless in a list. One call site turns it into a step.
import static org.wpilib.units.Units.Seconds; Command raiseArm = robot.arm.vertical() .until(() -> robot.arm.isAtTarget()) .named("vertical until at target") .withTimeout(Seconds.of(2.0));vertical() itself is untouched. 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(...), because .withTimeout(...) is a method on Command, not on the builder.
raiseArm is a Command like any other now, so it can be bound to a button or dropped into a routine. It ends itself either way.
What a timeout proves
That the waiting is over, and nothing else. If the next step assumes the arm arrived, ask robot.arm.isAtTarget() again before running it.
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. Fifteen seconds of autonomous go on step one.
- Tolerance too tight
- The arm settles half a degree outside the band and stops there. The plot looks fine. The routine does not move.
- A dead sensor
- A CANcoder off the bus reports one value forever, so
isAtTarget()gives the same answer every loop whatever the arm does. - 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 and 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.
- Add
isAtTarget()toArmandFlywheel, then bindraiseArmto a button withonTruein yourMyTeleop. It is a step now, so it ends itself. - Press it once and time how long the step takes to end.
- Change the arm's
tolerancetoDegrees.of(0.001)and press it again. - Leave the tolerance broken, drop
.withTimeout(...), and press it once more. - Put the tolerance back, add the Y binding, and hold Y. The arm goes up, and the flywheel starts only once the arm is there.
You should see
- The arm reaching its angle and the step ending well under two seconds.
- At
0.001, the step running the full two seconds every time. - With the timeout gone as well, the arm pushing until you disable.
- On Y, the flywheel waiting out the arm's travel, then spinning up with the arm still holding.
Write down the tolerance you settled on and how long the step took. Double that time for a backstop that will not fire on a good run. Coroutines asks for that number.
Check yourself
Why is the Y button a coroutine instead of Command.sequence(robot.arm.vertical().until(...), robot.flywheel.runFast())?
Why does isAtTarget() compare against a tolerance instead of checking whether the position equals the target?
What does () -> robot.arm.isAtTarget() hand to .until(...), and why does a bare robot.arm.isAtTarget() not work in the same place?
robot.arm.vertical().until(() -> robot.arm.isAtTarget()) on its own will not compile. What is missing?
A step ends after exactly the 2.0 seconds its .withTimeout(...) allowed. What do you know about the arm?
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?