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.
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.
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.
The flywheel already reports how fast it is turning. Compare that reading against the speed the step asked for, and the step can end once the wheel is up 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 robot.arm::isAtTarget, the condition passes the method itself, so it can be called again on every loop. Add the parentheses and robot.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 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, and getTargetPosition() asks the request object where it was last told to go. Both hand back an Angle rather than a bare number, so nothing on this line can mix up rotations and degrees.
getVelocity() reads the motor, and getTargetVelocity() asks the request object what speed it was last told to hold. Both hand back an AngularVelocity rather than a bare number, so nothing on this line can mix up rotations a second and RPM.
isNear does the comparison for you. It is true when the two are within tolerance of each other, and the tolerance is the number you pick. One degree is the arm's.Half a rotation a second is the flywheel's.
The flywheel answers the same question about speed. Same three methods, with AngularVelocity in place of Angle: getVelocity(), getTargetVelocity(), and a tolerance of RotationsPerSecond.of(0.5).
The arm answers the same question about angle. Same three methods, with Angle in place of AngularVelocity: getPosition(), getTargetPosition(), and a tolerance of Degrees.of(1.0).
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. isNear exists so you never write that comparison.
Both endings on one step
robot.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 = robot.arm.vertical() .until(robot.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 robot.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( robot.arm.vertical() .until(robot.arm::isAtTarget) .named("raise arm") .withTimeout(Seconds.of(2.0)), robot.flywheel.runFast().withTimeout(Seconds.of(1.0)), robot.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. robot.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()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.
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.
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 robot.arm::isAtTarget hand to .until(...), and why does 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?
Why is the last member of the sequence robot.flywheel.stop().withTimeout(Seconds.of(0.1)) rather than robot.flywheel.stop()?