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.armflywheel.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.
Like Command.sequence(...), .until(...) returns a builder, and .named("...") closes it.
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()robot.flywheel.runFast() is a hold. It re-sends its 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));import static org.wpilib.units.Units.Seconds; Command spinUp = robot.flywheel.runFast() .until(() -> robot.flywheel.isAtTarget()) .named("runFast until at target") .withTimeout(Seconds.of(2.0));The hold itself is untouched. The condition is the ending you want; the timeout is the ending you get when a sensor dies or the armflywheel jams. It goes after .named(...), because .withTimeout(...) is a method on Command, not on the builder.
What a timeout proves
That the waiting is over, and nothing else. If the next step assumes the armflywheel arrived, ask isAtTarget() again before running it.
Conditions that never come true
A condition that cannot go true is as bad as a bare hold. The step never ends, nothing throws, nothing logs, and the armflywheel 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 arm crosses the target for one loop on its way past. The step ends while it is still moving.
- Tolerance too tight
- The wheel settles at 74.3 rotations a second, outside the band, and stays there. The routine does not move.
- A dead sensor
- A motor off the bus reports one speed forever, so
isAtTarget()gives the same answer every loop whatever the wheel does. - Passing through
- A wheel that overshoots crosses 75 for one loop on the way up. The step ends before the speed has settled.
The timeout covers the first two and cannot help with the third. For a armflywheel 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 bindraiseArmspinUpto 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 armflywheel's
tolerancetoDegrees.of(0.001)RotationsPerSecond.of(0.001)and press it again. - Leave the tolerance broken, drop
.withTimeout(...), and press it once more. - Put the tolerance back and hold Y. The arm goes up, and the flywheel starts only once the arm is there.
You should see
- The armflywheel reaching its target 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 step never ending 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?