Gray Matter
WorkshopCommand Composition
Rough draft: nobody has reviewed this lesson yet, and it may not be how things are done this season.
LESSON 18

Command Composition

You have written commands that do one thing while a button is held. An autonomous routine is a list of those commands, run in order. This lesson builds the list.

12 minutes
You’ll need
  • An Arm with vertical() and horizontal(), and a Flywheel with runSlow(), runFast() and stop(), from Motion Magic in Code.
  • A MyTeleop with working button bindings, from OpModes.
  • The simulator running, from Hardware Simulation.

Every mechanism command you have written so far is a hold. runRepeatedly(...) re-sends the same request every loop and never ends.

So the work comes in two parts: give a command an ending, then glue the endings together in order.

Holds and steps

A hold never reports that it is finished, so a list containing one stops there for good. Step two never runs. Nothing errors and nothing logs, so the routine looks frozen.

.withTimeout(...) wraps a command and ends it after a fixed time, finished or not. That turns a hold into a step, something with a beginning and an end. Steps are what you can put in a list.

A hold, and the same hold as a step
import static org.wpilib.units.Units.Seconds;
 
// A hold. Drives to vertical and keeps holding it, never finishes.
robot.arm.vertical()
 
// A step. Holds vertical for one second, then ends.
robot.arm.vertical().withTimeout(Seconds.of(1.0))

The import static line is what lets you write Seconds rather than Units.Seconds every time. .withTimeout(...) refuses a bare 1.0. It takes a Time, so nobody can pass milliseconds where seconds were meant.

Steps in order

Command.sequence(a, b, c) runs a until it finishes, then b, then c. You write autonomous routines in exactly this shape.

MyTeleop.java: raise the arm, then spin up
Command spinUpWhenReady =
Command.sequence(
// A step: it ends, so the sequence moves on.
robot.arm.vertical().withTimeout(Seconds.of(1.0)),
// A hold: the last member, so the group is a hold too.
robot.flywheel.runFast())
.named("Spin Up When Ready (hold)");

A sequence is itself a small coroutine. Its body is one loop that calls coroutine.await(member) on each member in turn, and Coroutines writes that call by hand.

Command.sequence(...) returns a builder rather than a Command. .named("...") is what finishes it, and leaving it off will not compile. Name the group after what it does. If the group is a hold, end the name with (hold), the way the mechanism commands do.

Do not re-name a command that already has one. robot.arm.vertical() is already finished, so .named(...) on it is a compile error.

DON'T

A bare hold in the middle

Swap the first member for a plain robot.arm.vertical() and the sequence sticks there for the rest of the match. When a routine looks frozen, a member with no ending is the first thing to check.

Two things at once

A sequence is this, then that. A race is this while that. Command.race(...) starts every member at the same time and cancels the rest as soon as one finishes.

Spin the flywheel while the arm holds position
Command spinWhileHolding =
Command.race(
robot.flywheel.runFast().withTimeout(Seconds.of(2.0)),
robot.arm.horizontal())
.named("Spin While Holding Arm");

The flywheel member has an ending and the arm hold does not, so the flywheel decides when the group ends. A hold can never win a race. Other teams call this pattern a deadline. Commands v3 spells it Command.race(...), and there is no Command.deadline(...).

Bind the group

A group is a command, so it binds the same way a single command does. whileTrue runs the group while the button is held and cancels it on release. Canceling sends nothing to the motors, as Hardware Simulation showed, so the release sends a stop of its own.

MyTeleop.java: one button, both mechanisms
driver.y().whileTrue(spinUpWhenReady).whileFalse(robot.flywheel.stop());

The last member decides whether a group ends at all. End on a hold and the group is a hold. End on a step and the group finishes by itself, and the last request stays latched, so the stop belongs inside the group.

Check your work

  1. Add the Seconds import and the Spin Up When Ready (hold) binding to your MyTeleop constructor.
  2. Start the simulator and click Enable.
  3. Hold Y for three seconds, then release.
  4. Now break it on purpose. Drop .withTimeout(...) off the arm member, hold Y again, then put the timeout back.
  5. Bind Y to robot.flywheel.runFast().withTimeout(Seconds.of(1.0)) on its own instead and hold it for two seconds. The step ends after one second and the flywheel stays at 75 rotations per second, because nothing claimed it afterwards. A closing robot.flywheel.stop().withTimeout(Seconds.of(0.5)) step is the fix.
Check

You should see

  • The arm drives to vertical and holds there for one second, then the flywheel spins up.
  • The flywheel holds while Y is down and stops when you release it.
  • With the timeout gone, the arm reaches vertical and the flywheel never starts.

These three failures look nothing alike.

What you seeCause
The arm moves and the flywheel never startsA member with no ending ahead of it.
The group will not compileNo .named("..."), or .named(...) on a command that already had one.
Both mechanisms move togetherA Command.race where you meant Command.sequence.

Get this binding working in the simulator. Finish Conditions is next and replaces the stopwatch on the arm member with the arm's own report that it arrived.

Check yourself

You put robot.arm.vertical(), a hold with no timeout, as the first member of Command.sequence(...). What happens?

You bind Y to Command.sequence(stepA, stepB, robot.flywheel.runFast()), where stepA and stepB each end on a timeout. You hold Y for ten seconds. When does the group end?

In Command.race(robot.flywheel.runFast().withTimeout(Seconds.of(2.0)), robot.arm.horizontal()), what ends the group?

Your group's last member is a hold. You bind it with whileTrue and nothing else, then release the button. What does the flywheel do?

Pick an answer for each.