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 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.
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.
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.
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.
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.
- 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. 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.
- Add
isAtTarget()toArm, then bindraiseArmto a button withonTruein yourTeleopOpMode. It is a step now, so it ends itself. - Press it once and time how long the step takes to end.
- Set
POSITION_TOLERANCE_ROTto0.0001and press it again. - Leave the tolerance broken, drop
.withTimeout(...), and press it once more.
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
Why does isAtTarget() compare against a tolerance instead of checking whether the position equals the target?
What does arm::isAtTarget hand to .until(...), and why does arm.isAtTarget() not work in the same place?
arm.vertical().until(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?
Why is the last member of the sequence flywheel.stop().withTimeout(Seconds.of(0.1)) rather than flywheel.stop()?