Managing Transitions
From Diagram to Code: One switch, One Case per State
Lesson 25.4 designed the intake's state machine — four states, a start state, and a transition table — and gave the mechanism a single enum field, currentState. Turning the design into code takes one switch over that field, using the arrow form from Lesson 5.12. Each case does two jobs: it runs that state's state action, then checks the guard conditions of that state's outgoing transitions:
private void update()
{
switch (currentState)
{
case IDLE ->
{
motor.setThrottle(0.0);
if (intakeButton.getAsBoolean())
{
enter(IntakeState.INTAKING); // the way out of the start state
}
}
case INTAKING ->
{
motor.setThrottle(0.5);
if (pieceSensor.get())
{
enter(IntakeState.HOLDING);
}
}
case HOLDING ->
{
motor.setThrottle(0.1); // gentle hold pressure
if (scoreButton.getAsBoolean())
{
enter(IntakeState.SCORING);
}
}
case SCORING ->
{
motor.setThrottle(-0.5);
if (!pieceSensor.get())
{
enter(IntakeState.IDLE);
}
}
}
}
Compare it with the transition table: every row became one if inside the case for its "From" state. Here motor is a DriveMotor (Ch.4), pieceSensor is a DigitalInput (Ch.3), and intakeButton and scoreButton are BooleanSuppliers (Lesson 19.1) handed to the intake's constructor — a controller button's Trigger works, since a Trigger is a BooleanSupplier (Lesson 25.6). The arrow form can't fall through from one case into the next, so no break; is needed.
Transitions and Entry Actions Go Through One Method
A transition is just a new value in the state field — but routing every transition through one small method gives the machine a single place for entry actions, the things that should happen exactly once on entering a state:
private void enter(IntakeState next)
{
// entry action: log every change, once
System.out.println("Intake: " + currentState + " -> " + next);
currentState = next;
}
Logging every transition like this is one of the most useful debugging tools a state machine can have. A state-specific entry action goes here too — for example, restarting a timer when entering SCORING, so a guard in that case could check how long the intake has been scoring.
How the Machine Leaves Its Start State
Trace it from power-on. currentState starts as IDLE, so every loop runs the IDLE case: the rollers stay stopped, and the guard intakeButton.getAsBoolean() is checked. The first loop where the driver is pressing the intake button, the guard is true and enter(IntakeState.INTAKING) runs. From the next loop on, the INTAKING case runs instead. Outside inputs never assign currentState directly; they only feed the guards, and the machine decides for itself when to move.
Running the Machine Every Loop: A Default Command
A state machine has to be checked every loop, and in Commands v3 anything that controls a mechanism every loop belongs in a command. The natural fit is the mechanism's default command (Lesson 25.3), built with runRepeatedly so update() runs once per loop:
public Command stateMachineCommand()
{
return runRepeatedly(this::update)
.withPriority(Command.LOWEST_PRIORITY)
.named("Intake State Machine");
}
During setup (Lesson 25.7 shows where), it's installed once with intake.setDefaultCommand(intake.stateMachineCommand());. The lowest priority means any other command that requires the intake — say, a manual "eject" button for a stuck piece — can take over. While it does, update() doesn't run, but currentState is an ordinary field, so it keeps its value; when the other command ends, the default command starts again and the machine picks up in the state it was in.
Sidebar: v3's Built-In StateMachine (Alpha)
Commands v3 also includes a StateMachine class, where each state is a whole command and transitions are declared with methods like switchTo(...).when(...). It was added in May 2026 and changed again in September 2026, so it's still settling; this course teaches the enum-and-switch pattern above, which works the same in any version and is what the later subsystem chapters build on.
Common Pitfalls
- Putting transition logic in the wrong
case. A guard checked inside the wrong state's case either never triggers, or moves the machine out of a state it wasn't in. Eachifbelongs in the case for its transition's "From" state. - Assigning
currentStatedirectly instead of callingenter(...). A direct assignment skips the entry actions, and assignments scattered across many methods make it much harder to reason about when and why transitions happen — keep every transition inside the oneswitch, going throughenter(...). - Leaving a state out of the
switch. Aswitchstatement doesn't have to list every enum constant, so a missing case compiles fine — and a machine that enters that state does nothing and never leaves it. - Expecting the state machine to run while another command owns the mechanism. The default command pauses whenever a higher-priority command requires the mechanism; the state field keeps its value until the default command resumes.
Key Takeaways
- An arrow-form
switchover the state field, with one case per state, runs each state's action and checks only that state's guards. - Every row of the transition table becomes one
ifin its "From" state's case; routing transitions through oneenter(...)method gives entry actions (like logging) a single home. - The machine leaves its start state when a guard fed by an outside input — like a driver's button — becomes true; outside code feeds guards, it doesn't assign the state.
- In Commands v3, the
switchruns every loop as the mechanism's default command:runRepeatedly(this::update).withPriority(Command.LOWEST_PRIORITY).named(...). - v3's built-in
StateMachineclass exists but is alpha; the enum-and-switchpattern is the one this course builds on.
Derived from other-reference-repo: team-5817-training/subsystems/02-state-based-servo-subsystems.md, 03-state-based-roller-subsystems.md (T5817 31.1-4); v3 default-command placement and StateMachine sidebar from allwpilib main (commandsv3/…/command3/Mechanism.java, StateMachine.java)
Deck context: mechacoder-test/src/lessons/java-2.js, slide 11 ("Managing Transitions") — IntakeState switch example